JAl*_*lex 5 c# arrays performance simd avx
double[]我编写了一个函数来使用 SIMD 将数组的所有元素相加(System.Numerics.Vector,性能比 na\xc3\xafve 方法差。
在我的电脑上Vector<double>.Count是 4,这意味着我可以创建一个包含 4 个值的累加器,并运行数组,按组将元素相加。
例如,一个 10 元素数组,带有 4 元素累加器和 2 个剩余元素,我会得到
\n// | loop | remainder\nacc[0] = vector[0] + vector[4] + vector[8]\nacc[1] = vector[1] + vector[5] + vector[9]\nacc[2] = vector[2] + vector[6] \nacc[3] = vector[3] + vector[7] \nRun Code Online (Sandbox Code Playgroud)\n和结果sum = acc[0]+acc[1]+acc[2]+acc[3]
下面的代码产生了正确的结果,但与仅将值相加的循环相比,速度不高
\npublic static double SumSimd(this Span<double> a)\n{\n var n = System.Numerics.Vector<double>.Count;\n var count = a.Length;\n // divide array into n=4 element groups\n // Example, 57 = 14*4 + 3\n var groups = Math.DivRem(count, n, out int remain);\n var buffer = new double[n];\n // Create buffer with remaining elements (not in groups)\n a.Slice(groups*n, remain).CopyTo(buffer);\n // Scan through all groups and accumulate\n var accumulator = new System.Numerics.Vector<double>(buffer);\n for (int i = 0; i < groups; i++)\n {\n //var next = new System.Numerics.Vector<double>(a, n * i);\n var next = new System.Numerics.Vector<double>(a.Slice(n * i, n));\n accumulator += next;\n }\n var sum = 0.0;\n // Add up the elements of the accumulator vs\n for (int j = 0; j < n; j++)\n {\n sum += accumulator[j];\n }\n return sum;\n}\nRun Code Online (Sandbox Code Playgroud)\n所以我的问题是为什么我没有意识到 SIMD 的任何好处?
\n基线代码如下所示
\npublic static double LinAlgSum(this ReadOnlySpan<double> span)\n{\n double sum = 0;\n for (int i = 0; i < span.Length; i++)\n {\n sum += span[i];\n }\n return sum;\n}\nRun Code Online (Sandbox Code Playgroud)\n在对 SIMD 代码进行基准测试时,与上述代码相比,SIMD 代码慢 5\xc3\x97 size=7, 慢 2.5\xc3\x97 size=144, 大约相同size=770。
我正在使用 运行发布模式BenchmarkDotNet。这是驱动程序类
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]\n[CategoriesColumn]\npublic class LinearAlgebraBench\n{\n [Params(7, 35, 77, 144, 195, 311, 722)]\n public int Size { get; set; }\n\n [IterationSetup]\n public void SetupData()\n {\n A = new LinearAlgebra.Vector(Size, (iter) => 2 * Size - iter).ToArray();\n B = new LinearAlgebra.Vector(Size, (iter) => Size/2 + 2* iter).ToArray();\n }\n\n public double[] A { get; set; }\n public double[] B { get; set; }\n\n [BenchmarkCategory("Sum"), Benchmark(Baseline = true)]\n public double BenchLinAlgSum()\n {\n return LinearAlgebra.LinearAlgebra.Sum(A.AsSpan().AsReadOnly());\n }\n [BenchmarkCategory("Sum"), Benchmark]\n public double BenchSimdSum()\n {\n return LinearAlgebra.LinearAlgebra.SumSimd(A);\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n
我建议您看一下这篇文章,探讨.Net 中的 SIMD 性能。
使用常规向量化进行求和的整体算法看起来相同。一个区别是,在对数组进行切片时可以避免乘法:
while (i < lastBlockIndex)
{
vresult += new Vector<int>(source.Slice(i));
i += Vector<int>.Count;
}
Run Code Online (Sandbox Code Playgroud)
一次乘法对性能的影响应该相当小,但对于这种代码来说,它可能是相关的。
还值得注意的是,编译器似乎无法使用通用 API 生成非常高效的 SIMD 代码。对 32768 项求和的性能:
因此,SIMD 的通用版本仅获得约 30% 的性能,而本征版本获得约 400% 的性能,接近理论最大值。
根据@JonasH 的回答
还值得注意的是,编译器似乎无法使用通用 API 生成非常高效的 SIMD 代码。
我不同意。唯一值得确保该方法得到正确实施。在某些情况下 - 是的,直接使用Intrinsics而不是Numerics矢量会带来很大的提升,但并非总是如此。
这里的问题是测量非常小的迭代。一般情况下Benchmark.NET做不到。可能的解决方案是将目标方法包装在循环中。
对我来说,编写一个有代表性的基准测试是一项艰苦的工作,而且我可能还不够擅长。但我会尝试。
public class SumTest
{
[Params(7, 35, 77, 144, 195, 311, 722)]
public int Size { get; set; }
[IterationSetup]
public void SetupData()
{
A = Enumerable.Range(0, Size).Select(x => 1.1).ToArray();
}
public double[] A { get; set; }
[BenchmarkCategory("Sum"), Benchmark(Baseline = true)]
public double BenchScalarSum()
{
double result = 0;
for (int i = 0; i < 10000; i++)
result = SumScalar(A);
return result;
}
[BenchmarkCategory("Sum"), Benchmark]
public double BenchNumericsSum()
{
double result = 0;
for (int i = 0; i < 10000; i++)
result = SumNumerics(A);
return result;
}
[BenchmarkCategory("Sum"), Benchmark]
public double BenchIntrinsicsSum()
{
double result = 0;
for (int i = 0; i < 10000; i++)
result = SumIntrinsics(A);
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double SumScalar(ReadOnlySpan<double> numbers)
{
double result = 0;
for (int i = 0; i < numbers.Length; i++)
{
result += numbers[i];
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double SumNumerics(ReadOnlySpan<double> numbers)
{
ReadOnlySpan<Vector<double>> vectors = MemoryMarshal.Cast<double, Vector<double>>(numbers);
Vector<double> acc = Vector<double>.Zero;
for (int i = 0; i < vectors.Length; i++)
{
acc += vectors[i];
}
double result = Vector.Dot(acc, Vector<double>.One);
for (int i = vectors.Length * Vector<double>.Count; i < numbers.Length; i++)
result += numbers[i];
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double SumIntrinsics(ReadOnlySpan<double> numbers)
{
ReadOnlySpan<Vector256<double>> vectors = MemoryMarshal.Cast<double, Vector256<double>>(numbers);
Vector256<double> acc = Vector256<double>.Zero;
for (int i = 0; i < vectors.Length; i++)
{
acc = Avx.Add(acc, vectors[i]);
}
Vector128<double> r = Sse2.Add(acc.GetUpper(), acc.GetLower());
double result = Sse3.HorizontalAdd(r, r).GetElement(0); // I'm aware that VHADDPD probably not enough efficient but leaving it for simplicity here
for (int i = vectors.Length * Vector256<double>.Count; i < numbers.Length; i++)
result += numbers[i];
return result;
}
}
Run Code Online (Sandbox Code Playgroud)
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-4700HQ CPU 2.40GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=5.0.203
[Host] : .NET Core 5.0.6 (CoreCLR 5.0.621.22011, CoreFX 5.0.621.22011), X64 RyuJIT
Job-NQCIIR : .NET Core 5.0.6 (CoreCLR 5.0.621.22011, CoreFX 5.0.621.22011), X64 RyuJIT
Run Code Online (Sandbox Code Playgroud)
| 方法 | 尺寸 | 意思是 | 错误 | 标准差 | 中位数 | 比率 | 比率SD |
|---|---|---|---|---|---|---|---|
| 基准标量和 | 7 | 53.34 我们 | 0.056 微秒 | 0.050我们 | 53.30 我们 | 1.00 | 0.00 |
| 基准数值总和 | 7 | 48.95 我们 | 2.262 我们 | 6.671 我们 | 44.95 我们 | 0.95 | 0.10 |
| 工作台内在总和 | 7 | 55.85 我们 | 2.089 我们 | 6.128 我们 | 51.90 我们 | 1.07 | 0.10 |
| 基准标量和 | 35 | 258.46 我们 | 2.319 我们 | 3.541 我们 | 257.00 我们 | 1.00 | 0.00 |
| 基准数值总和 | 35 | 94.14 我们 | 1.989 我们 | 5.705 我们 | 91.00 我们 | 0.36 | 0.02 |
| 工作台内在总和 | 35 | 90.82 我们 | 2.465 我们 | 7.073 我们 | 92.10 我们 | 0.35 | 0.03 |
| 基准标量和 | 77 | 541.18 我们 | 10.401 我们 | 11.129 我们 | 536.95 我们 | 1.00 | 0.00 |
| 基准数值总和 | 77 | 161.05 我们 | 3.171 我们 | 7.475 我们 | 159.30 我们 | 0.30 | 0.01 |
| 工作台内在总和 | 77 | 153.19 我们 | 3.063 我们 | 7.906 我们 | 150.50 我们 | 0.29 | 0.02 |
| 基准标量和 | 144 | 1,166.72 美元 | 6.945 我们 | 5.422 我们 | 1,166.10 我们 | 1.00 | 0.00 |
| 基准数值总和 | 144 | 294.72 我们 | 5.675 我们 | 10.520 我们 | 292.50 我们 | 0.26 | 0.01 |
| 工作台内在总和 | 144 | 287.18 我们 | 5.661 我们 | 13.671 我们 | 284.20 我们 | 0.25 | 0.01 |
| 基准标量和 | 195 | 1,671.83 美元 | 32.634 我们 | 34.918 我们 | 1,663.30 我们 | 1.00 | 0.00 |
| 基准数值总和 | 195 | 443.19 我们 | 7.916 我们 | 11.354 我们 | 443.10 我们 | 0.26 | 0.01 |
| 工作台内在总和 | 195 | 444.21 我们 | 8.876 我们 | 7.868 我们 | 443.55 我们 | 0.27 | 0.01 |
| 基准标量和 | 311 | 2,742.78 美元 | 35.797 我们 | 29.892 我们 | 2,745.70 我们 | 1.00 | 0.00 |
| 基准数值总和 | 311 | 778.00 我们 | 34.173 我们 | 100.759 我们 | 719.20 我们 | 0.30 | 0.04 |
| 工作台内在总和 | 311 | 776.30 我们 | 29.304 我们 | 86.404 我们 | 727.45 我们 | 0.29 | 0.03 |
| 基准标量和 | 第722章 | 6,607.72 美元 | 79.263 我们 | 74.143 我们 | 6,601.20 我们 | 1.00 | 0.00 |
| 基准数值总和 | 第722章 | 1,870.81 美元 | 43.390 我们 | 127.936 我们 | 1,850.30 我们 | 0.28 | 0.02 |
| 工作台内在总和 | 第722章 | 1,867.57 美元 | 39.718 我们 | 117.110 我们 | 1,851.50 我们 | 0.28 | 0.02 |
看起来使用向量至少不比基线方法效率低。
作为奖励,让我们使用https://sharplab.io/ (x64)查看输出汇编代码
SumTest.SumScalar(System.ReadOnlySpan`1<Double>)
L0000: vzeroupper
L0003: mov rax, [rdx]
L0006: mov edx, [rdx+8]
L0009: vxorps xmm0, xmm0, xmm0
L000d: xor ecx, ecx
L000f: test edx, edx
L0011: jle short L0022
L0013: movsxd r8, ecx
L0016: vaddsd xmm0, xmm0, [rax+r8*8]
L001c: inc ecx
L001e: cmp ecx, edx
L0020: jl short L0013
L0022: ret
SumTest.SumNumerics(System.ReadOnlySpan`1<Double>)
L0000: sub rsp, 0x28
L0004: vzeroupper
L0007: mov rax, [rdx]
L000a: mov edx, [rdx+8]
L000d: mov ecx, edx
L000f: shl rcx, 3
L0013: shr rcx, 5
L0017: cmp rcx, 0x7fffffff
L001e: ja short L0078
L0020: vxorps ymm0, ymm0, ymm0
L0024: xor r8d, r8d
L0027: test ecx, ecx
L0029: jle short L0040
L002b: movsxd r9, r8d
L002e: shl r9, 5
L0032: vaddpd ymm0, ymm0, [rax+r9]
L0038: inc r8d
L003b: cmp r8d, ecx
L003e: jl short L002b
L0040: vmulpd ymm0, ymm0, [SumTest.SumNumerics(System.ReadOnlySpan`1<Double>)]
L0048: vhaddpd ymm0, ymm0, ymm0
L004c: vextractf128 xmm1, ymm0, 1
L0052: vaddpd xmm0, xmm0, xmm1
L0056: shl ecx, 2
L0059: cmp ecx, edx
L005b: jge short L0070
L005d: cmp ecx, edx
L005f: jae short L007e
L0061: movsxd r8, ecx
L0064: vaddsd xmm0, xmm0, [rax+r8*8]
L006a: inc ecx
L006c: cmp ecx, edx
L006e: jl short L005d
L0070: vzeroupper
L0073: add rsp, 0x28
L0077: ret
L0078: call 0x00007ffc9de2b710
L007d: int3
L007e: call 0x00007ffc9de2bc70
L0083: int3
SumTest.SumIntrinsics(System.ReadOnlySpan`1<Double>)
L0000: sub rsp, 0x28
L0004: vzeroupper
L0007: mov rax, [rdx]
L000a: mov edx, [rdx+8]
L000d: mov ecx, edx
L000f: shl rcx, 3
L0013: shr rcx, 5
L0017: cmp rcx, 0x7fffffff
L001e: ja short L0070
L0020: vxorps ymm0, ymm0, ymm0
L0024: xor r8d, r8d
L0027: test ecx, ecx
L0029: jle short L0040
L002b: movsxd r9, r8d
L002e: shl r9, 5
L0032: vaddpd ymm0, ymm0, [rax+r9]
L0038: inc r8d
L003b: cmp r8d, ecx
L003e: jl short L002b
L0040: vextractf128 xmm1, ymm0, 1
L0046: vaddpd xmm0, xmm1, xmm0
L004a: vhaddpd xmm0, xmm0, xmm0
L004e: shl ecx, 2
L0051: cmp ecx, edx
L0053: jge short L0068
L0055: cmp ecx, edx
L0057: jae short L0076
L0059: movsxd r8, ecx
L005c: vaddsd xmm0, xmm0, [rax+r8*8]
L0062: inc ecx
L0064: cmp ecx, edx
L0066: jl short L0055
L0068: vzeroupper
L006b: add rsp, 0x28
L006f: ret
L0070: call 0x00007ffc9de2b710
L0075: int3
L0076: call 0x00007ffc9de2bc70
L007b: int3
Run Code Online (Sandbox Code Playgroud)
在这里您可以看到 JIT 为 生成几乎相同的Vector<T>代码Vector256<T>。