mcpField
V
vectorization

vectorization

github.com/dotnet开发5.8k2.1k 次安装1 天前更新
代码仓库

关于 vectorization

.NET SIMD vectorization 面向使用 SIMD 优化 .NET 代码的开发者,提供编写可移植且内存安全实现的规则,要求保持标量行为,并明确测试与基准测量要求。

主要功能

  1. 01优先使用现有加速框架 API
  2. 02遵循可移植 SIMD 分派规则
  3. 03安全处理尾部与缓冲区重叠
  4. 04测试标量和向量路径
  5. 05与标量实现进行基准比较

适用场景

  1. 01检查框架 API 后优化数值循环
  2. 02验证 SIMD 边界情况和重叠处理
  3. 03衡量更宽向量路径是否值得采用

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dotnet/skills --skill vectorization

安装后在 Claude Code 中输入/vectorization确认已加载。

详细介绍

.NET SIMD 向量化

实现可移植的优化,在保持标量契约的同时,确保所有长度下的内存安全,并通过实测结果证明其复杂度值得。请先阅读官方的 SIMD 和硬件内置函数指南, 并遵循其中全面的实现模板。尤其要采用其中按位宽自包含的分派方式、专门的小输入处理、循环和余数处理结构,而不是将其简化为一连串位宽检查。本技能提供了在修改实际代码时应采用的决策规则和验证检查。

输入和前提条件

在询问用户之前,先从代码库中确认以下内容:

输入必需需要确认的内容
标量实现和测试是现有契约、具有代表性的调用位置,以及支持的重叠情况
目标框架和平台是可用的 SIMD API,以及必须保持一致行为的架构
构建和测试工作流是代码库的常规命令,以及如何启动独立的测试进程
具有代表性的工作负载或基准测试优化时需要典型输入大小和要超越的基线

不要仅仅因为某个 API 存在于某个包中就添加该包。应先检查目标框架和项目现有的依赖项及版本管理策略。

核心规则

  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。只有在实测发现性能差距时才使用架构专用内置函数,并用 IsSupported 进行保护,同时保留等效的可移植或标量行为。
  4. 直接读取 IsHardwareAccelerated、IsSupported 和 Count。 JIT 会将它们视为常量,因此缓存它们毫无价值,还会掩盖哪些分支会被消除。
  5. 表达清晰时优先使用运算符。 混用位运算符和比较运算符时,请用括号明确优先级。

如果任务仅限于审查,请勿重写代码。应先报告正确性和内存安全缺陷,再讨论性能优化机会。

编写检查清单

  • **契约:**在更改实现之前,先明确空输入和短输入、重叠、溢出、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 表示待实现的操作:
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 可在存在重叠写入时保留原始值。对于只读归约,在主循环之后加载相同的最终范围,并使用 ConditionalSelect 将已处理的通道替换为该操作的单位元。不要仅为避免跨度边界检查而改用 LoadUnsafe/StoreUnsafe 或标量收尾代码。

测试清单

  • 针对边界长度、随机值、空输入、支持的重叠情况和数值边界情况,将优化实现与标量契约进行比较。覆盖所有已实现的宽度以及标量路径,并使用足够大和过小、无法从中获益的输入。
  • 在独立进程中测试每种已实现的宽度和标量回退路径。在 x86/x64 CoreCLR 上,DOTNET_EnableAVX2=0 可禁用 AVX2,DOTNET_EnableHWIntrinsic=0 可禁用硬件内在函数。使用仓库的常规测试命令,不要在单元测试中更改这些进程级设置。这些设置不会更改已编译为 ReadyToRun 或提前编译的代码,因此用它们强制选择某条路径时,请确认目标代码由 JIT 编译。
  • 对于不安全加载和存储,在条件允许时使用保护页或等效的边界测试。正向迭代时,将不可访问页放在缓冲区之后;反向迭代时,将其放在缓冲区之前,并包含非整数倍长度。普通数组分配无法可靠地暴露越界读取。

基准测试

使用 BenchmarkDotNet 测量有代表性的小型和大型输入,再决定是否保留新增的复杂性。在同一次运行中比较标量、Vector128<T> 和每种已实现的更宽路径。小型输入可能更慢,因为开销主要来自准备工作;受内存吞吐量、对齐和延迟影响,实际加速通常达不到理论上的向量宽度倍数。报告吞吐量或耗时,并说明噪声背景;相关时,还应报告生成代码大小或指令数。为获得稳定测量,需控制分配对齐;也可随机化对齐,以观察其分布。更宽的向量并不一定更快。

如果项目无法面向所需框架、运行相关架构或执行回退配置,请准确说明哪些路径仍未验证。不要仅凭默认硬件上的测试就宣称成功。

完成约定

  • 编写: 确保测试覆盖标量契约;说明所选框架或 SIMD 层;报告有代表性工作负载的测量结果;指出任何未能测试的架构或回退路径。
  • 审查: 仅报告具体问题,并依正确性、内存安全、可移植性、测试、性能证据的顺序排列。若没有问题,请直接说明。
  • 如果优化仅能构建、仅在当前机器上通过,或没有与标量基线进行比较,就不要称其已完成。

审查清单

按以下顺序审查:

  1. 是否等同于标量契约,包括有符号零、NaN、溢出和相关的字节序
  2. 是否复用了现有的加速框架 API
  3. 幂等与非幂等操作的尾部处理是否正确
  4. 内存安全、无符号偏移运算、空输入和缓冲区重叠
  5. 可移植的分派以及行为等效的回退路径
  6. 能否通过测试强制执行每种宽度和标量路径
  7. 基准测试是否证明显式 SIMD 和增加更多宽度的合理性

评论

支持 Markdown