vectorization

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

.NET SIMD vectorization

.NET SIMD 向量化

Produce a portable optimization that preserves the scalar contract, remains memory-safe at every length, and earns its complexity with measured results. Read the official SIMD and hardware-intrinsics guidance first and follow its comprehensive implementation templates. In particular, use its self-contained per-width dispatch, dedicated small-input handling, loop, and remainder shapes rather than reducing them to a chain of width checks. This skill supplies the decision rules and validation checks to apply while changing real code.
生成可移植的优化方案,既要保留标量契约,在任何长度下都保持内存安全,又要通过实测结果证明其复杂度的合理性。请先阅读官方的SIMD 和硬件 intrinsics 指南,并遵循其中全面的实现模板。尤其要使用其独立的按宽度分发、专用的小输入处理、循环和余数处理结构,而非将它们简化为一连串的宽度检查。本技能提供了修改实际代码时需遵循的决策规则和验证检查。

Inputs and prerequisites

输入与前提条件

Discover these from the repository before asking the user:
InputRequiredWhat to establish
Scalar implementation and testsYesExisting contract, representative call sites, and supported overlap
Target frameworks and platformsYesAvailable SIMD APIs and architectures that must behave consistently
Build and test workflowYesThe repository's normal commands and how to launch separate test processes
Representative workload or benchmarkFor optimizationTypical input sizes and the baseline to beat
Do not add a package merely because an API exists there. First check the target framework and the project's existing dependency/versioning policy.
在询问用户之前,先从代码仓库中确认以下信息:
输入项是否必填需要确认的内容
标量实现与测试用例现有契约、代表性调用场景以及支持的内存重叠情况
目标框架与平台可用的SIMD API以及必须保持行为一致的架构
构建与测试工作流仓库的常规命令以及如何启动独立测试进程
代表性工作负载或基准测试优化时需要典型输入大小以及需要超越的基准性能
不要仅仅因为某个API存在就添加对应的包。首先检查目标框架以及项目现有的依赖/版本控制策略。

Core rules

核心规则

  1. Use the highest-level API that matches the contract, then stop.
    Span<T>
    and
    string
    operations,
    TensorPrimitives
    , and tensor types already accelerate many operations. LINQ reductions such as
    Sum
    ,
    Min
    ,
    Max
    , and
    Average
    can also accelerate when the source exposes its underlying span. Verify empty-input and floating-point behavior rather than assuming similarly named operations are interchangeable. Once an existing API preserves the contract, use it instead of continuing into handwritten SIMD. Before writing an explicit loop, name the framework APIs considered and why none applies. Fixed-shape
    System.Numerics
    types remain appropriate for graphics and similar domains.
  2. Start new explicit SIMD loops with
    Vector128<T>
    .
    It is accelerated across the broadest hardware set. Add wider fixed-width paths only when measurements justify them.
  3. Keep platforms consistent. Prefer cross-platform operations on the fixed-width vector types; they lower to the appropriate target instructions. For example,
    (vector & mask) == Vector128<byte>.Zero
    becomes
    ptest
    on x86/x64. Use architecture-specific intrinsics only for a measured gap, guard them with
    IsSupported
    , and retain equivalent portable or scalar behavior.
  4. Read
    IsHardwareAccelerated
    ,
    IsSupported
    , and
    Count
    directly.
    The JIT treats them as constants, so caching them adds no value and obscures which branches disappear.
  5. Prefer operators where they are clear. Parenthesize expressions that mix bitwise and comparison operators so precedence is explicit.
If the task is review-only, do not rewrite the code. Report correctness and memory-safety defects before performance opportunities.
  1. 使用符合契约的最高层级API,然后停止优化。
    Span<T>
    string
    操作、
    TensorPrimitives
    以及张量类型已为许多操作提供了加速。当源数据暴露其底层span时,LINQ聚合操作如
    Sum
    Min
    Max
    Average
    也能实现加速。要验证空输入和浮点行为,不要假设名称相似的操作具有可互换性。一旦现有API能保留契约,就使用它,无需继续手写SIMD代码。在编写显式循环之前,列出考虑过的框架API并说明为何它们都不适用。固定形状的
    System.Numerics
    类型仍适用于图形及类似领域。
  2. 新的显式SIMD循环从
    Vector128<T>
    开始。
    它在最广泛的硬件集上都能获得加速。只有当实测结果证明有必要时,再添加更宽的固定宽度路径。
  3. 保持平台一致性。 优先在固定宽度向量类型上使用跨平台操作;它们会降级为目标平台的相应指令。例如,
    (vector & mask) == Vector128<byte>.Zero
    在x86/x64上会转换为
    ptest
    指令。仅当实测存在性能差距时才使用特定架构的intrinsics,并用
    IsSupported
    进行保护,同时保留等效的可移植或标量行为。
  4. 直接读取
    IsHardwareAccelerated
    IsSupported
    Count
    JIT会将它们视为常量,因此缓存这些值没有任何意义,还会掩盖哪些分支会被消除。
  5. 在清晰易懂的情况下优先使用运算符。 对混合了位运算和比较运算符的表达式添加括号,明确优先级。
如果任务仅为代码审查,请勿重写代码。先报告正确性和内存安全缺陷,再提及性能优化机会。

Authoring checklist

编写清单

  • Contract: identify behavior for empty and short inputs, overlap, overflow, NaN, signed zero, ordering, and exceptions before changing the implementation.
  • Framework gate: inspect the target framework and existing package references, then compile or probe the highest-level candidate API with the required edge cases. A small contract adapter, such as preserving special empty-input behavior, does not justify reimplementing the operation. If the API preserves the contract, use it and stop; do not claim it is unavailable without checking.
  • Structure: for new explicit SIMD, implement
    Vector128<T>
    and scalar first. Only after measurements justify wider paths, check
    Vector512<T>
    , then
    Vector256<T>
    , optional
    Vector<T>
    ,
    Vector128<T>
    , and finally scalar. Omit paths the implementation does not need. Each outer fixed-width guard checks only its
    IsHardwareAccelerated
    property and, for generic element types,
    IsSupported
    . Inside that block, run the width-specific helper when the input has at least
    Count
    elements; otherwise run a dedicated small-input helper, then return. Do not put the length check in the outer guard and fall through to repeat dispatch at narrower widths. Keeping each supported-width block self-contained lets the JIT remove unsupported blocks and avoids redundant work on common small inputs.
  • Loads and stores: prefer span-based
    Vector128.Create(span)
    and
    CopyTo
    ; the JIT keeps them efficient and they require no pinning or reference arithmetic. Unsafe loads and stores are largely unnecessary. When a path genuinely must walk a buffer by managed reference, use the element-offset
    LoadUnsafe(ref T, nuint)
    and
    StoreUnsafe
    overloads rather than pointers or manually advanced references.
  • Empty inputs: in a reference-based path, obtain the starting reference with
    MemoryMarshal.GetReference(span)
    or
    MemoryMarshal.GetArrayDataReference(array)
    , not by indexing element
    0
    .
  • Unsupported element types: the fixed-width vectors support primitive numeric element types, not
    char
    or
    bool
    . Reinterpret with
    MemoryMarshal.Cast
    or
    As<TFrom, TTo>
    ; reinterpretation changes only the type, not the bits. Keep Boolean data as
    0
    or
    1
    and characters as valid UTF-16, normalizing results before storing when necessary.
  • Offsets: prove the input contains a full vector before subtracting
    Count
    or converting an index to
    nuint
    ; otherwise a negative value becomes a huge unsigned offset.
  • Managed references: do not form references before the start or past the end of a span, including a one-past-end reference. The runtime permits a non-dereferenced managed pointer exactly one past an object or array, but this guidance intentionally prohibits the pattern because it is fragile and easy to misuse. Keep the base reference in range and express traversal with an element offset.
  • Remainders: cover every length, including
    0
    ,
    Count - 1
    ,
    Count
    ,
    Count + 1
    , and nonmultiples of each width. Once the input contains a full vector, keep the tail vectorized by reprocessing the last full vector. An idempotent operation can fold that overlap in directly. A non-idempotent operation must use
    ConditionalSelect
    to replace repeated lanes with the operation's identity before folding them in. This is the JIT-recognized general pattern; it can reduce a zero-identity selection to a bitwise mask while retaining broader optimization opportunities. For in-place transforms, preserve the original tail values before overlapping stores and write only valid results.
  • Buffer overlap: choose a traversal direction or staging strategy that prevents stores from corrupting values not yet loaded.
  • Numeric behavior: account for floating-point reassociation, NaN and signed-zero semantics, checked or unchecked integer overflow, and endianness where the algorithm depends on byte order.
    Native
    and
    Estimate
    operations can intentionally relax precision or IEEE edge-case behavior; use them only when the contract permits it and measurements justify them.
The official guidance contains the complete dispatch, small-input, unrolling, and remainder templates; use those for the full implementation. The following excerpt illustrates only the inner safe
Vector128<T>
loop for an in-place elementwise transform, after its self-contained dispatch block has established at least one full vector.
Transform
represents the operation being implemented:
csharp
Span<int> tail = data.Slice(data.Length - Vector128<int>.Count);
Vector128<int> end = Vector128.Create<int>(tail);
Span<int> remaining = data;

while (remaining.Length >= Vector128<int>.Count)
{
    Vector128<int> values = Vector128.Create<int>(remaining);
    Transform(values).CopyTo(remaining);
    remaining = remaining.Slice(Vector128<int>.Count);
}

if (!remaining.IsEmpty)
{
    Transform(end).CopyTo(tail);
}
The early
end
load preserves original values before overlapping stores. For a read-only reduction, load the same final span after the main loop and use
ConditionalSelect
to replace already-processed lanes with the operation's identity. Do not substitute
LoadUnsafe
/
StoreUnsafe
or a scalar epilogue merely to avoid span bounds checks.
  • 契约: 在修改实现之前,明确空输入、短输入、内存重叠、溢出、NaN、符号零、排序和异常的处理行为。
  • 框架检查: 检查目标框架和现有包引用,然后编译或测试最高层级候选API的必要边缘情况。小型契约适配(如保留特殊的空输入行为)不足以证明需要重新实现该操作。如果API能保留契约,就使用它并停止优化;未检查之前不要声称该API不可用。
  • 结构: 对于新的显式SIMD代码,先实现
    Vector128<T>
    和标量路径。只有当实测结果证明需要更宽的路径时,才依次检查
    Vector512<T>
    Vector256<T>
    (可选的
    Vector<T>
    )、
    Vector128<T>
    ,最后是标量路径。省略实现不需要的路径。每个外部固定宽度检查块仅需验证其
    IsHardwareAccelerated
    属性,对于泛型元素类型,还需验证
    IsSupported
    。在该块内部,当输入长度至少为
    Count
    时运行宽度特定的辅助函数;否则运行专用的小输入辅助函数,然后返回。不要将长度检查放在外部检查块中,再降级到更窄宽度的分发路径。保持每个支持宽度的块独立,JIT就能移除不支持的块,避免在常见小输入上做冗余工作。
  • 加载与存储: 优先使用基于span的
    Vector128.Create(span)
    CopyTo
    ;JIT会保持它们的高效性,且无需固定内存或引用算术。不安全的加载和存储在很大程度上是不必要的。当某个路径确实需要通过托管引用遍历缓冲区时,使用基于元素偏移的
    LoadUnsafe(ref T, nuint)
    StoreUnsafe
    重载,而非指针或手动推进的引用。
  • 空输入: 在基于引用的路径中,使用
    MemoryMarshal.GetReference(span)
    MemoryMarshal.GetArrayDataReference(array)
    获取起始引用,而非通过索引元素
    0
  • 不支持的元素类型: 固定宽度向量支持原始数值元素类型,不支持
    char
    bool
    。使用
    MemoryMarshal.Cast
    As<TFrom, TTo>
    进行重新解释;重新解释仅改变类型,不改变位模式。将布尔数据保持为
    0
    1
    ,字符保持为有效的UTF-16,必要时在存储前归一化结果。
  • 偏移量: 在减去
    Count
    或将索引转换为
    nuint
    之前,要证明输入包含完整的向量;否则负值会变为巨大的无符号偏移量。
  • 托管引用: 不要创建超出span起始位置或结束位置的引用,包括超出末尾一个位置的引用。运行时允许存在未解引用的托管指针恰好指向对象或数组的末尾之后,但本指南明确禁止这种模式,因为它很脆弱且容易误用。保持基础引用在范围内,使用元素偏移量来表示遍历。
  • 余数处理: 覆盖所有长度,包括
    0
    Count - 1
    Count
    Count + 1
    以及每个宽度的非倍数。一旦输入包含完整向量,通过重新处理最后一个完整向量来保持尾部的向量化。幂等操作可以直接合并重叠部分。非幂等操作必须使用
    ConditionalSelect
    ,在合并之前用操作的单位元替换重复的通道。这是JIT可识别的通用模式;它可以将单位元选择简化为位掩码,同时保留更广泛的优化机会。对于原地转换,在重叠存储之前保留原始尾部值,仅写入有效的结果。
  • 缓冲区重叠: 选择遍历方向或暂存策略,防止存储操作破坏尚未加载的值。
  • 数值行为: 考虑浮点重新关联、NaN和符号零语义、已检查或未检查的整数溢出,以及算法依赖字节顺序时的字节序。
    Native
    Estimate
    操作可以有意放宽精度或IEEE边缘情况行为;仅当契约允许且实测结果证明有必要时才使用它们。
官方指南包含完整的分发、小输入、循环展开和余数处理模板;请使用这些模板完成完整实现。以下片段仅展示了原地元素级转换的内部安全
Vector128<T>
循环,前提是其独立的分发块已确认输入至少包含一个完整向量。
Transform
代表正在实现的操作:
csharp
Span<int> tail = data.Slice(data.Length - Vector128<int>.Count);
Vector128<int> end = Vector128.Create<int>(tail);
Span<int> remaining = data;

while (remaining.Length >= Vector128<int>.Count)
{
    Vector128<int> values = Vector128.Create<int>(remaining);
    Transform(values).CopyTo(remaining);
    remaining = remaining.Slice(Vector128<int>.Count);
}

if (!remaining.IsEmpty)
{
    Transform(end).CopyTo(tail);
}
提前加载
end
可以在重叠存储之前保留原始值。对于只读聚合操作,在主循环之后加载相同的最终span,并使用
ConditionalSelect
用操作的单位元替换已处理的通道。不要仅仅为了避免span边界检查而替换为
LoadUnsafe
/
StoreUnsafe
或标量收尾代码。

Testing checklist

测试清单

  • Compare the optimized implementation with the scalar contract across boundary lengths, randomized values, empty inputs, supported overlap, and numeric edge cases. Cover every implemented width and the scalar path with inputs both large enough and too small to benefit.
  • Exercise every implemented width and the scalar fallback in separate processes. On x86/x64 CoreCLR,
    DOTNET_EnableAVX2=0
    disables AVX2 and
    DOTNET_EnableHWIntrinsic=0
    disables hardware intrinsics. Use the repository's normal test command and do not change these process-wide settings inside a unit test. These settings do not change code already compiled as ReadyToRun or ahead of time, so confirm the target code is JIT-compiled when using them to force a path.
  • For unsafe loads and stores, use guard-page or equivalent boundary tests when available. Put the inaccessible page after the buffer for forward iteration and before it for backwards iteration, and include nonmultiple lengths. An ordinary array allocation does not reliably expose an out-of-bounds read.
  • 在边界长度、随机值、空输入、支持的内存重叠和数值边缘情况下,对比优化后的实现与标量契约。使用足够大的输入和太小而无法受益的输入,覆盖所有已实现的宽度和标量路径。
  • 在独立进程中测试所有已实现的宽度和标量降级路径。在x86/x64 CoreCLR上,
    DOTNET_EnableAVX2=0
    会禁用AVX2,
    DOTNET_EnableHWIntrinsic=0
    会禁用硬件intrinsics。使用仓库的常规测试命令,不要在单元测试内部修改这些进程级设置。这些设置不会改变已编译为ReadyToRun或提前编译的代码,因此在使用它们强制测试某条路径时,要确认目标代码是JIT编译的。
  • 对于不安全的加载和存储,在可用时使用保护页或等效的边界测试。对于正向遍历,将不可访问的页放在缓冲区之后;对于反向遍历,放在缓冲区之前,并包含非倍数长度。普通数组分配无法可靠地暴露越界读取问题。

Benchmarking

基准测试

Use BenchmarkDotNet to measure representative small and large inputs before keeping the added complexity. Compare scalar,
Vector128<T>
, and each wider implemented path in the same run. Small inputs can be slower because setup dominates, and speedups are rarely the theoretical vector-width multiple because memory throughput, alignment, and latency still apply. Report throughput or time with noise context and, when relevant, generated code size or instruction counts. Control allocation alignment for stable measurements or randomize it to observe the distribution. A wider vector is not automatically faster.
If the project cannot target the required framework, run the relevant architecture, or execute the fallback configuration, state exactly which path remains unverified. Do not claim success from a default-hardware test alone.
使用BenchmarkDotNet测量代表性的小输入和大输入性能,再决定是否保留新增的复杂度。在同一轮测试中对比标量、
Vector128<T>
以及每个已实现的更宽路径。小输入可能更慢,因为初始化开销占主导,且加速比很少能达到理论上的向量宽度倍数,因为内存吞吐量、对齐和延迟仍会产生影响。报告吞吐量或时间时要包含误差范围,必要时还要报告生成的代码大小或指令计数。控制分配对齐以获得稳定的测量结果,或随机化对齐以观察分布情况。更宽的向量并不一定更快。
如果项目无法针对所需框架、运行相关架构或执行降级配置,请明确说明哪条路径仍未验证。不要仅通过默认硬件测试就声称优化成功。

Completion contract

完成契约

  • Authoring: leave the scalar contract covered by tests; identify the framework or SIMD layer selected; report measurements for the representative workload; name any architecture or fallback path that could not be exercised.
  • Review: report only concrete findings, ordered by correctness, memory safety, portability, tests, then performance evidence. If none remain, say so directly.
  • Do not call an optimization complete when it only builds, only passes on the current machine, or has no comparison against the scalar baseline.
  • 编写: 确保标量契约仍被测试覆盖;说明选择的框架或SIMD层级;报告代表性工作负载的实测结果;列出任何无法测试的架构或降级路径。
  • 审查: 仅报告具体发现,按正确性、内存安全、可移植性、测试、性能证据的顺序排列。如果没有问题,请直接说明。
  • 当优化仅能编译通过、仅在当前机器上通过测试或未与标量基准进行对比时,不要声称优化已完成。

Review checklist

审查清单

Review in this order:
  1. Scalar-contract equivalence, including signed zero, NaN, overflow, and relevant endianness
  2. Reuse of an existing accelerated framework API
  3. Tail correctness for idempotent versus non-idempotent work
  4. Memory safety, unsigned offset arithmetic, empty inputs, and overlapping buffers
  5. Portable dispatch and behaviorally equivalent fallbacks
  6. Tests that force each width and the scalar path
  7. Benchmarks that justify explicit SIMD and additional widths
按以下顺序进行审查:
  1. 标量契约一致性,包括符号零、NaN、溢出和相关字节序
  2. 是否复用了现有的加速框架API
  3. 幂等与非幂等工作的尾部处理正确性
  4. 内存安全、无符号偏移量运算、空输入和重叠缓冲区
  5. 可移植分发和行为等效的降级方案
  6. 能强制测试每个宽度和标量路径的测试用例
  7. 证明显式SIMD和额外宽度必要性的基准测试