你可以用这种方式用 3+3 双打进行单次操作
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector3D Substract(Vector3D left, Vector3D right)
{
Vector256<double> v0 = Vector256.Create(left.X, left.Y, left.Z, 0);
Vector256<double> v1 = Vector256.Create(right.X, right.Y, right.Z, 0);
Vector256<double> result = Avx.Subtract(v0, v1);
return new Vector3D(result.GetElement(0), result.GetElement(1), result.GetElement(2));
}
MethodImplOptions.AggressiveInlining 告诉编译器将方法的代码嵌入到调用者的主体中(如果可能的话)。输出程序集中没有方法调用,只有计算。
它可能会更快,但您的测试有 2 个问题。
- 不要在每个操作中检查
Avx2.IsSupported,而是在每个应用程序生命周期中检查一次。
- 不要在循环中创建数据,内存分配会使测试变得又慢又脏。
干净的测试可以是这样的
[Benchmark]
public void SubtractBenchMarkAccelerated()
{
Vector3D vector1 = new Vector3D(1.5, 2.5, 3.5);
Vector3D vector2 = new Vector3D(0.1, 0.2, 0.3);
for (int i = 0; i < 1000000; i++)
{
Subtract(vector1, vector2);
}
}
但是如果只使用Vector256 容量的75% 会出现单次操作的问题。能快25%吗?是的,还有更多数据。
这只是故事的开始。假设您想一次计算 4 组向量。 4 对 4。表演魔术从哪里开始。
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector3D[] SubstractArray(Vector3D[] left, Vector3D[] right)
{
var v0 = MemoryMarshal.Cast<Vector3D, Vector256<double>>(left);
var v1 = MemoryMarshal.Cast<Vector3D, Vector256<double>>(right);
Vector3D[] result = new Vector3D[left.Length];
var r = MemoryMarshal.Cast<Vector3D, Vector256<double>>(result);
for (int i = 0; i < v0.Length; i++) // v0.Length = 3 here, not 4
{
r[i] = Avx.Subtract(v0[i], v1[i]);
}
return result;
}
MemoryMarshal.Cast 不会复制任何内容,它只是让Span<T> 指向与源数组相同的内存,因此速度快如闪电。我测试过。
测试可能如下所示。
[Benchmark]
public void SubtractBenchMarkAccelerated4()
{
Vector3D[] array1 = new Vector3D[4];
array1[0] = new Vector3D(1.5, 2.5, 3.5);
array1[1] = new Vector3D(1.5, 2.5, 3.5);
array1[2] = new Vector3D(1.5, 2.5, 3.5);
array1[3] = new Vector3D(1.5, 2.5, 3.5);
Vector3D[] array2 = new Vector3D[4];
array2[0] = new Vector3D(0.1, 0.2, 0.3);
array2[1] = new Vector3D(0.1, 0.2, 0.3);
array2[2] = new Vector3D(0.1, 0.2, 0.3);
array2[3] = new Vector3D(0.1, 0.2, 0.3);
for (int i = 0; i < 1000000; i++)
{
SubstractArray(array1, array2);
}
}
在与 1000000 相同的时间内计算 4000000 个向量,为什么不呢?您可以通过这种方式计算任意数量的向量。请确保doubles count % 4 == 0。
它可以比上面的例子更快吗?是的,但只有不安全的代码。
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe Vector3D[] SubstractArrayUnsafe(Vector3D[] left, Vector3D[] right)
{
var v0 = MemoryMarshal.Cast<Vector3D, Vector256<double>>(left);
var v1 = MemoryMarshal.Cast<Vector3D, Vector256<double>>(right);
Vector3D[] result = new Vector3D[left.Length];
var r = MemoryMarshal.Cast<Vector3D, Vector256<double>>(result);
fixed (Vector256<double>* vPtr0 = v0, vPtr1 = v1, rPtr = r)
{
Vector256<double>* endPtr0 = vPtr0 + v0.Length;
Vector256<double>* vPos0 = vPtr0;
Vector256<double>* vPos1 = vPtr1;
Vector256<double>* rPos = rPtr;
while (vPos0 < endPtr0)
{
*rPos = Avx.Subtract(*vPos0, *vPos1);
vPos0++;
vPos1++;
rPos++;
}
}
return result;
}
您不仅可以通过这种方式减去 Vector3D[],还可以减去您的 Vector4D[] 或简单的 double[] 数组。
还可以访问这些有用的页面:x86/x64 SIMD Instruction List (SSE to AVX512) 和 this one。
更新
为相同大小的包优化单个操作
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector4DHW Substract(ref Vector4DHW left, ref Vector4DHW right)
{
var left1 = Unsafe.As<Vector4DHW, Vector256<double>>(ref left);
var right1 = Unsafe.As<Vector4DHW, Vector256<double>>(ref right);
var result = Avx.Subtract(left1, right1);
return Unsafe.As<Vector256<double>, Vector4DHW>(ref result);
}
让我们进行基准测试
class Program
{
static void Main()
{
var summary = BenchmarkRunner.Run<MyBenchmark>();
Console.ReadKey();
}
}
[StructLayout(LayoutKind.Sequential)]
public struct Vector4DHW
{
public double X;
public double Y;
public double Z;
public double W;
public Vector4DHW(double x, double y, double z, double w)
{
X = x;
Y = y;
Z = z;
W = w;
}
}
public class MyBenchmark
{
private Vector4DHW vector1 = new Vector4DHW(1.5, 2.5, 3.5, 4.5);
private Vector4DHW vector2 = new Vector4DHW(0.1, 0.2, 0.3, 0.4);
[Benchmark]
public void Loop()
{
for (int i = 0; i < 1000000; i++)
{
var j = i;
}
}
[Benchmark]
public void Substract()
{
for (int i = 0; i < 1000000; i++)
{
var result = Substract(ref vector1, ref vector2);
}
}
[Benchmark]
public void SubstractAvx()
{
for (int i = 0; i < 1000000; i++)
{
var result = SubstractAvx(ref vector1, ref vector2);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector4DHW Substract(ref Vector4DHW left, ref Vector4DHW right)
{
return new Vector4DHW(left.X - right.X, left.Y - right.Y, left.Z - right.Z, left.W - right.W);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector4DHW SubstractAvx(ref Vector4DHW left, ref Vector4DHW right)
{
var left1 = Unsafe.As<Vector4DHW, Vector256<double>>(ref left);
var right1 = Unsafe.As<Vector4DHW, Vector256<double>>(ref right);
var result = Avx.Subtract(left1, right1);
return Unsafe.As<Vector256<double>, Vector4DHW>(ref result);
}
}
去吧!
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.102
[Host] : .NET Core 5.0.2 (CoreCLR 5.0.220.61120, CoreFX 5.0.220.61120), X64 RyuJIT
DefaultJob : .NET Core 5.0.2 (CoreCLR 5.0.220.61120, CoreFX 5.0.220.61120), X64 RyuJIT
| Method | Mean | Error | StdDev |
|------------- |-----------:|--------:|--------:|
| Loop | 317.6 us | 1.36 us | 1.21 us |
| Substract | 1,427.0 us | 4.14 us | 3.46 us |
| SubstractAvx | 478.0 us | 1.58 us | 1.40 us |
作为结论,我可以说,当您试图节省更多微秒的性能时,内存优化非常重要。甚至堆栈分配也很重要,无论其闪电般的速度如何。最后,for 循环开销从478 微秒开始消耗大量时间。这就是我单独测量Loop 开销的原因。
让我们计算 AVX 的性能增益。
1,427.0 - 317.6 = 1109.4
478.0 - 317.6 = 160.4
1109.4 / 160.4 = 6.92
AVX 几乎快 7 倍。
更新2
也测试一下
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe static Vector4DHW Substract(Vector4DHW left, Vector4DHW right)
{
var result = Avx.Subtract(*(Vector256<double>*)&left, *(Vector256<double>*)&right);
return *(Vector4DHW*)&result;
}