我看过几篇文章描述了如何Vector<T>
支持 SIMD 并使用 JIT 内在函数实现,因此编译器在使用它时将正确输出 AVS/SSE/... 指令,从而允许比经典的线性循环更快的代码(例如here https://www.codeproject.com/Articles/1223361/Benchmarking-NET-Core-SIMD-performance-vs-Intel-IS).
我决定尝试重写一个方法,看看我是否能够获得一些加速,但到目前为止我失败了,矢量化代码的运行速度比原始代码慢了 3 倍,我不太确定为什么。这是检查两个是否存在的方法的两个版本Span<float>
实例的所有项目对都处于同一位置,并且相对于阈值共享相同的位置。
// Classic implementation
public static unsafe bool MatchElementwiseThreshold(this Span<float> x1, Span<float> x2, float threshold)
{
fixed (float* px1 = &x1.DangerousGetPinnableReference(), px2 = &x2.DangerousGetPinnableReference())
for (int i = 0; i < x1.Length; i++)
if (px1[i] > threshold != px2[i] > threshold)
return false;
return true;
}
// Vectorized
public static unsafe bool MatchElementwiseThresholdSIMD(this Span<float> x1, Span<float> x2, float threshold)
{
// Setup the test vector
int l = Vector<float>.Count;
float* arr = stackalloc float[l];
for (int i = 0; i < l; i++)
arr[i] = threshold;
Vector<float> cmp = Unsafe.Read<Vector<float>>(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<float>
v1 = Unsafe.Read<Vector<float>>(px1 + offset),
v1cmp = Vector.GreaterThan<float>(v1, cmp),
v2 = Unsafe.Read<Vector<float>>(px2 + offset),
v2cmp = Vector.GreaterThan<float>(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 < x1.Length; i++)
if (px1[i] > threshold != px2[i] > threshold)
return false;
}
return true;
}
正如我所说,我已经使用 BenchmarkDotNet 测试了两个版本,并且使用了Vector<T>
运行速度比另一台慢 3 倍左右。我尝试使用不同长度的跨度(从大约 100 到超过 2000)运行测试,但矢量化方法始终比另一种方法慢得多。
我在这里遗漏了一些明显的东西吗?
Thanks!
EDIT:我使用不安全代码并尝试在不并行化的情况下尽可能优化该代码的原因是该方法已经在Parallel.For
迭代。
另外,具有在多个线程上并行化代码的能力通常并不是不优化各个并行任务的好理由。