Guidance for writing and reviewing SIMD / hardware-intrinsics code in dotnet/runtime. USE FOR: vectorizing a scalar algorithm, writing or reviewing code that uses Vector128/Vector256/Vector512, Vector<T>, or the platform intrinsics in System.Runtime.Intrinsics.X86/Arm/Wasm, and validating remainder handling, load/store safety, and hardware-acceleration fallbacks. DO NOT USE FOR: general performance work unrelated to SIMD (use performance-benchmark), or non-vectorized code review (use code-review).
79
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
The general, cross-cutting guidance for SIMD and hardware intrinsics lives in the official .NET documentation. Read it first and defer to it for anything not specific to this repo:
The repo-specific nuance is in docs/coding-guidelines/vectorization-guidelines.md.
This skill distills what to actually enforce when authoring or reviewing vectorized changes here.
Span<T>/string methods,
TensorPrimitives, and the tensor types already vectorize many operations. LINQ is often vectorized
too — operators such as Sum, Max, Min, and Average accelerate when the source's underlying
span can be extracted. Don't hand-roll what's already optimized and tested.Vector128<T>. It's the common denominator accelerated on the broadest hardware, and
you don't need Vector256/Vector512 for a correct, portable implementation. Add wider widths and
platform intrinsics only for a measured hot path.Vector128/Vector256; they lower
to the optimal instruction per target (for example (vector & mask) == Vector128<byte>.Zero becomes
ptest on x86/x64). Only drop to System.Runtime.Intrinsics.X86/Arm/Wasm when a specific
instruction measurably beats the portable form, and guard it with the class's IsSupported.IsHardwareAccelerated and Count directly; don't cache them to locals. Both are JIT-time
constants, so caching buys nothing, and a local obscures that constant-ness — read them at each use
so the branches you don't take are eliminated.+, &, <<) for readability, but mind precedence:
a & b == c parses as a & (b == c), so parenthesize when mixing them.Vector128.IsHardwareAccelerated and
Vector128<T>.IsSupported (the latter matters in generic code), then compare length against Count.Vector128.Create(span) / CopyTo — the JIT keeps them
efficient and they need no pinning or reference arithmetic. The unsafe load/store variants are
largely no longer needed; when you genuinely must walk a buffer by managed reference, use the
LoadUnsafe(ref T, nuint elementOffset) / StoreUnsafe element-offset overloads rather than raw
pointer or ref arithmetic.MemoryMarshal.GetReference (or
GetArrayDataReference for arrays), not ref span[0].Vector128<T> supports the primitive numerics, not char or
bool. Reinterpret via MemoryMarshal.Cast (a span) or the vector's As<TFrom, TTo> — for example
char → ushort. Reinterpretation changes only the type, not the bits, so keeping the data
well-formed is on you (a bool stays 0/1, a char a valid UTF-16 code unit); normalize any
out-of-range result before writing it back.nuint). Always check the buffer length before computing an offset
like buffer.Length - Vector128<int>.Count; if the buffer is smaller than one vector that subtraction
underflows to a huge value.ConditionalSelect first.ref point outside its buffer, even
transiently — a GC that runs at that moment won't update it, producing a GC hole. See the
LastIndexOf case study (#73768 /
fix).Vector256 path, the Vector128 path, and the scalar path — each with
inputs both large enough and too small to benefit.DOTNET_EnableAVX2=0 (disables Vector256), and with
DOTNET_EnableHWIntrinsic=0 (disables all intrinsics down to the software fallback). Build the
affected library and run its test project per the build/test workflow in
.github/copilot-instructions.md, with the relevant
DOTNET_Enable* variable set in the environment.BoundedMemory.
BoundedMemory.Allocate<T>(count)
places a no-access page immediately after the buffer (use PoisonPagePlacement.Before for
backwards-iterating algorithms), so on most targets a read past the end faults with an access
violation instead of silently succeeding. It falls back to an unprotected allocation on Browser/WASI
and .NET Framework, so don't rely on the guard there. Always include lengths that aren't an exact
multiple of the vector width.Vectorization adds complexity, so measure that it pays off before keeping it. Use BenchmarkDotNet
and the same DOTNET_Enable* variables to compare scalar / Vector128 / Vector256 in one run. Keep
in mind: larger inputs benefit more (small buffers can be slower due to setup), speedups are rarely
the theoretical multiple (memory throughput, alignment, and latency all factor in), and randomized
allocation alignment adds noise — allocate aligned memory or enable BenchmarkDotNet's randomization for
stable/observable results. For non-trivial changes, use the performance-benchmark skill.
When reviewing a vectorized change, verify in priority order:
BitConverter.IsLittleEndian) edge cases? Verify any claim about existing behavior.nuint underflow, no ref straying outside its buffer, empty
buffers handled, overlap considered.BoundedMemory) and run under the
acceleration-toggle env vars? Ask for the missing test rather than just rejecting.ba10a6e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.