Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion docs/standard/simd.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,74 @@ Types that aren't supported, such as `char` and `bool`, can still be vectorized

## Cross-platform vectorization with Vector128

`Vector128<T>` is the common denominator across every platform that supports vectorization, so it's the best place to start. It holds a 128-bit vector: 16 bytes, 8 shorts, 4 ints/floats, or 2 longs/doubles. `Vector256<T>` is twice as wide, and `Vector512<T>` twice again. Not all hardware supports the larger widths, so the examples that follow use `Vector128` for portability.
`Vector128<T>` is the common denominator across every platform that supports vectorization, so it's the best place to start. It holds a 128-bit vector: 16 bytes, 8 shorts, 4 ints/floats, or 2 longs/doubles.

```
------------------------------128-bits---------------------------
| 64 | 64 |
-----------------------------------------------------------------
| 32 | 32 | 32 | 32 |
-----------------------------------------------------------------
| 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 |
-----------------------------------------------------------------
| 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 |
-----------------------------------------------------------------
```

`Vector256<T>` is twice as wide, and `Vector512<T>` twice again. Not all hardware supports the larger widths, so the examples that follow use `Vector128` for portability.

Each width has a generic type (`Vector128<T>`) for the data and a non-generic static class (<xref:System.Runtime.Intrinsics.Vector128>) that holds most of the operations, including static factory methods like `Create` and `Load`. Operators such as `+`, `&`, and `<<` are the idiomatic way to express arithmetic and bit operations; prefer them over the named method equivalents to avoid operator-precedence bugs and improve readability. For algorithms that depend on byte order, branch on <xref:System.BitConverter.IsLittleEndian>, which the JIT also folds to a constant.

> [!NOTE]
> On x86/x64, `Vector256<T>` operations are generally treated as two independent 128-bit "lanes". For most element-wise operations this is transparent, but operations that cross lanes (such as shuffles or pairwise/horizontal operations) might behave differently or cost more than the `Vector128` equivalent. Confirm with benchmarks before assuming a wider vector is faster.

### Lane-crossing operations don't widen for free

Element-wise operations don't care about width: `v1 + v2` is the same per-element result whether `v1` and `v2` are `Vector128<T>` or `Vector256<T>`—widening just processes one more lane's worth of data per instruction. `Add` on `v = [a, b, c, d]` and `w = [e, f, g, h]` always combines same-index elements:

```
v: [ a | b | c | d ]
w: [ e | f | g | h ]
+ + + +
r: [a+e|b+f|c+g|d+h]
```

Lane-crossing operations don't scale up that simply, because which elements get combined depends on the vector's width. A pairwise reduction combines *adjacent* elements instead of same-index ones, so widening changes what ends up paired together:

```
v: [ a | b | c | d ]
\_____/ \_____/
round 1: [ a+b | c+d | a+b | c+d ]
\_________________/
round 2: [ S | S | S | S ] (S = a+b+c+d)
```

That's exactly what a horizontal reduction does: sum a vector's elements with two rounds of pairwise adds. On x86/x64, `Vector128<float>` (4 elements) gets you there with two calls to <xref:System.Runtime.Intrinsics.X86.Sse3.HorizontalAdd*>:

:::code language="csharp" source="./snippets/simd/csharp/LaneCrossingExample.cs" id="SumVector128":::

Extend the same two-call pattern to `Vector256<float>` (8 elements) and it looks right—it isn't. <xref:System.Runtime.Intrinsics.X86.Avx.HorizontalAdd*> doesn't operate across the whole 256-bit vector; it repeats the pairwise pattern independently within each 128-bit lane. Two rounds gives you the lower lane's sum (elements 0-3) broadcast across the lower lane and the upper lane's sum (elements 4-7) broadcast across the upper lane, not the sum of all eight elements:

:::code language="csharp" source="./snippets/simd/csharp/LaneCrossingExample.cs" id="SumVector256Naive":::

To get the correct total, bridge the lane boundary explicitly: read each lane's partial sum with `GetLower`/`GetUpper` and add them together—`GetLower` and `GetUpper` split a vector into its first and second halves:

```
------------------------------128-bits---------------------------
| LOWER | UPPER |
-----------------------------------------------------------------
| 32 | 32 | 32 | 32 |
-----------------------------------------------------------------
| 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 |
-----------------------------------------------------------------
| 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 |
-----------------------------------------------------------------
```

:::code language="csharp" source="./snippets/simd/csharp/LaneCrossingExample.cs" id="SumVector256":::

That extra step is the real cost of crossing lanes. A lane-crossing algorithm doesn't just widen for free the way an element-wise one does—measure before assuming the wider vector wins.

### Common operations

`Vector128` and its wider siblings expose a large API surface. You don't need to memorize it—know the categories and look up the details when you need them. Every operation has a software fallback for platforms that can't accelerate it. The following table covers essentially the whole surface.
Expand Down
57 changes: 57 additions & 0 deletions docs/standard/snippets/simd/csharp/LaneCrossingExample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Diagnostics;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;

namespace SimdSnippets;

public static class LaneCrossingExample
{
// <SumVector128>
// Sums all four elements with two rounds of pairwise horizontal adds.
// HorizontalAdd(v, v) on [a, b, c, d] gives [a+b, c+d, a+b, c+d]; a second round
// collapses that to the full sum in every element.
public static float SumVector128(Vector128<float> v)
{
Debug.Assert(Sse3.IsSupported);

Vector128<float> step1 = Sse3.HorizontalAdd(v, v);
Vector128<float> step2 = Sse3.HorizontalAdd(step1, step1);

return step2.ToScalar();
}
// </SumVector128>

// <SumVector256Naive>
// The same two-round pattern on Vector256<float> looks like it should sum all eight
// elements, but Avx.HorizontalAdd repeats the pairwise pattern independently within
// each 128-bit lane. The result holds the lower lane's sum (elements 0-3) broadcast
// across the lower lane and the upper lane's sum (elements 4-7) broadcast across the
// upper lane -- ToScalar only returns the lower lane's partial sum, not the total.
public static float SumVector256Naive(Vector256<float> v)
{
Debug.Assert(Avx.IsSupported);

Vector256<float> step1 = Avx.HorizontalAdd(v, v);
Vector256<float> step2 = Avx.HorizontalAdd(step1, step1);

return step2.ToScalar();
}
// </SumVector256Naive>

// <SumVector256>
// Getting the full sum needs an explicit step to cross the lane boundary: read each
// lane's partial sum out with GetLower/GetUpper and add them together.
public static float SumVector256(Vector256<float> v)
{
Debug.Assert(Avx.IsSupported);

Vector256<float> step1 = Avx.HorizontalAdd(v, v);
Vector256<float> step2 = Avx.HorizontalAdd(step1, step1);

Vector128<float> lower = step2.GetLower();
Vector128<float> upper = step2.GetUpper();

return lower.ToScalar() + upper.ToScalar();
}
// </SumVector256>
}
15 changes: 15 additions & 0 deletions docs/standard/snippets/simd/csharp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Numerics;
using System.Linq;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SimdSnippets;

// Verification runner. Not part of the published snippets.
Expand Down Expand Up @@ -49,3 +50,17 @@
float[] c = [1, 1, 1, 1];
Console.WriteLine($"MultiplyAdd = [{string.Join(", ", TensorPrimitivesExample.MultiplyAdd(a, b, c))}]");
Console.WriteLine($"CosineSimilarity(a, a) = {TensorPrimitivesExample.CosineSimilarity(a, a)}");
Console.WriteLine();

if (Sse3.IsSupported)
{
Vector128<float> v128 = Vector128.Create(1f, 2f, 3f, 4f);
Console.WriteLine($"SumVector128 = {LaneCrossingExample.SumVector128(v128)}");
}

if (Avx.IsSupported)
{
Vector256<float> v256 = Vector256.Create(1f, 2f, 3f, 4f, 10f, 20f, 30f, 40f);
Console.WriteLine($"SumVector256Naive = {LaneCrossingExample.SumVector256Naive(v256)}");
Console.WriteLine($"SumVector256 = {LaneCrossingExample.SumVector256(v256)}");
}
Loading