使用Vector 运行比经典循环慢的SIMD矢量化C#代码

我已经看过一些文章,描述了Vector是如何启用SIMD并使用JIT内在函数实现的,因此编译器在使用时会正确输出AVS / SSE / …指令,允许比经典线性循环更快的代码( 这里的例子)。

我决定尝试重写一个方法,我必须看看我是否设法获得了一些加速,但到目前为止我失败了,矢量化代码的运行速度比原来快3倍,而且我不确定为什么。 以下是两个版本的方法,检查两个Span实例是否具有相同位置的所有项对,它们相对于阈值共享相同的位置。

 // Classic implementation public static unsafe bool MatchElementwiseThreshold(this Span x1, Span x2, float threshold) { fixed (float* px1 = &x1.DangerousGetPinnableReference(), px2 = &x2.DangerousGetPinnableReference()) for (int i = 0; i  threshold != px2[i] > threshold) return false; return true; } // Vectorized public static unsafe bool MatchElementwiseThresholdSIMD(this Span x1, Span x2, float threshold) { // Setup the test vector int l = Vector.Count; float* arr = stackalloc float[l]; for (int i = 0; i < l; i++) arr[i] = threshold; Vector cmp = Unsafe.Read<Vector>(arr); fixed (float* px1 = &x1.DangerousGetPinnableReference(), px2 = &x2.DangerousGetPinnableReference()) { // Iterate in chunks int div = x1.Length / l, mod = x1.Length % l, i = 0, offset = 0; for (; i < div; i += 1, offset += l) { Vector v1 = Unsafe.Read<Vector>(px1 + offset), v1cmp = Vector.GreaterThan(v1, cmp), v2 = Unsafe.Read<Vector>(px2 + offset), v2cmp = Vector.GreaterThan(v2, cmp); float* pcmp1 = (float*)Unsafe.AsPointer(ref v1cmp), pcmp2 = (float*)Unsafe.AsPointer(ref v2cmp); for (int j = 0; j < l; j++) if (pcmp1[j] == 0 != (pcmp2[j] == 0)) return false; } // Test the remaining items, if any if (mod == 0) return true; for (i = x1.Length - mod; i  threshold != px2[i] > threshold) return false; } return true; } 

正如我所说的,我使用BenchmarkDotNet测试了两个版本,使用Vector版本比另一个版本慢了3倍。 我尝试使用不同长度的跨度(从大约100到超过2000)运行测试,但是矢量化方法比另一个慢得多。

我错过了一些明显的东西吗?

谢谢!

编辑:为什么我使用不安全的代码并尝试尽可能地优化此代码而不并行化它的原因是这个方法已经从Parallel.For迭代中调用。

此外,具有在多个线程上并行化代码的能力通常不是使各个并行任务不被优化的好理由。

**编辑**在阅读Marc Gravell的博客文章后 ,我发现这可以简单地实现……

 public static bool MatchElementwiseThresholdSIMD(ReadOnlySpan x1, ReadOnlySpan x2, float threshold) { if (x1.Length != x2.Length) throw new ArgumentException("x1.Length != x2.Length"); if (Vector.IsHardwareAccelerated) { var vx1 = x1.NonPortableCast>(); var vx2 = x2.NonPortableCast>(); var vthreshold = new Vector(threshold); for (int i = 0; i < vx1.Length; ++i) { var v1cmp = Vector.GreaterThan(vx1[i], vthreshold); var v2cmp = Vector.GreaterThan(vx2[i], vthreshold); if (Vector.Xor(v1cmp, v2cmp) != Vector.Zero) return false; } x1 = x1.Slice(Vector.Count * vx1.Length); x2 = x2.Slice(Vector.Count * vx2.Length); } for (var i = 0; i < x1.Length; i++) if (x1[i] > threshold != x2[i] > threshold) return false; return true; } 

现在这不像直接使用数组那么快(如果这就是你所拥有的),但仍然比非SIMD版本快得多……

(另一个编辑……)

…而且只是为了好玩,我想我会很好地看到这些东西在完全通用时处理工作,答案非常好……所以你可以编写如下代码,它就像特定的一样高效(好吧)除非在非硬件加速的情况下,在这种情况下它的速度不到两倍 – 但并不完全可怕 ……)

  public static bool MatchElementwiseThreshold(ReadOnlySpan x1, ReadOnlySpan x2, T threshold) where T : struct { if (x1.Length != x2.Length) throw new ArgumentException("x1.Length != x2.Length"); if (Vector.IsHardwareAccelerated) { var vx1 = x1.NonPortableCast>(); var vx2 = x2.NonPortableCast>(); var vthreshold = new Vector(threshold); for (int i = 0; i < vx1.Length; ++i) { var v1cmp = Vector.GreaterThan(vx1[i], vthreshold); var v2cmp = Vector.GreaterThan(vx2[i], vthreshold); if (Vector.AsVectorInt32(Vector.Xor(v1cmp, v2cmp)) != Vector.Zero) return false; } // slice them to handling remaining elementss x1 = x1.Slice(Vector.Count * vx1.Length); x2 = x2.Slice(Vector.Count * vx1.Length); } var comparer = System.Collections.Generic.Comparer.Default; for (int i = 0; i < x1.Length; i++) if ((comparer.Compare(x1[i], threshold) > 0) != (comparer.Compare(x2[i], threshold) > 0)) return false; return true; } 

矢量只是一个矢量。 它不声称或保证使用SIMD扩展。 使用

System.Numerics.Vector2

https://docs.microsoft.com/en-us/dotnet/standard/numerics#simd-enabled-vector-types