C# .Net SIMD System.Numerics.Vector4 比循环慢

C# .Net SIMD System.Numerics.Vector4 slower than loop

我编写了以下代码来试验 System.Numerics.Vector4 并评估性能增益:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Numerics;

namespace ConsoleApp8
{
    class Program
    {
        static void Main(string[] args)
        {
            const int N = 100000000;
            long ticks_start, ticks_end;

            ticks_start = DateTime.Now.Ticks;

            float[] a = { 10, 10, 10, 10 };
            float[] b = new float[4];

            for (int i = 0; i < N; i++)
                for (int j = 0; j < 4; j++)
                    b[j] = a[j] + a[j];

            ticks_end = DateTime.Now.Ticks;


            Console.WriteLine($"Done in {ticks_end - ticks_start} ticks");

            ticks_start = DateTime.Now.Ticks;

            Vector4 result;
            Vector4 v = new Vector4();
            for (int i = 0; i < N; i++)
            {
                v.W = a[0];
                v.X = a[1];
                v.Y = a[2];
                v.Z = a[3];
                result = Vector4.Add(v, v);
                b[0] = result.W;
                b[1] = result.X;
                b[2] = result.Y;
                b[3] = result.Z;
            }
            ticks_end = DateTime.Now.Ticks;
            Console.WriteLine($"Done in {ticks_end - ticks_start} ticks");

            Console.ReadKey();
        }
    }
}

输出为:

Done in 14257591 ticks
Done in 18591588 ticks

所以看起来我们使用 Vector4 没有任何优势。 Add 方法 returns Vector4 的新实例。有没有办法改变其中一个向量以避免内存分配影响?或者也许有另一种方法来做事?

我还没有真正对其进行基准测试,但是在内部循环中:

for (int i = 0; i < N; i++)
{

    v.W = a[0];
    v.X = a[1];
    v.Y = a[2];
    v.Z = a[3];
    result = Vector4.Add(v, v);
    b[0] = result.W;
    b[1] = result.X;
    b[2] = result.Y;
    b[3] = result.Z;
}

更等同于:


float[] a = { 10, 10, 10, 10 };
float[] b = new float[4];
float[] v = new float[4];
float[] result = new float[4];

for (int i = 0; i < N; i++)
{
    v[0] = a[0];
    v[1] = a[1];
    v[2] = a[2];
    v[3] = a[3];
    result[0] = v[0] + v[0];
    result[1] = v[1] + v[1];
    result[2] = v[2] + v[2];
    result[3] = v[3] + v[3];
    b[0] = result[0];
    b[1] = result[1];
    b[2] = result[2];
    b[3] = result[3];
}

比你写的要多(你正在做 4 个赋值 - 赋值给 v-,然后是加法,然后是另外 4 个赋值 - 赋值 resultb-,你只是跳过你的数组操作)。

我刚刚使用秒表在 linqpad 上对其进行了测试(这绝不是基准测试),如果你这样做,数组添加(即使总和展开)比 Vector4(以非常小的幅度)。

根据评论区的各种建议,在release模式下测试,向量化版本明显比数组版本快。

发布此答案是为了提高可见度。感谢 Jeroen van Langen、phuclv 和 Alexei Levenkov。

感谢他们花时间和耐心回答这个问题。

可以在不使用分配的情况下改进代码。 MemoryMarshal.Cast 从字面上创建一个 Span 指向与数组相同的内存区域。 float 数组的大小可以是任意的,它将产生大小为 a.Length / 4Vector4Span

结果相同。当您写入输出 Span 时,数据将直接写入输出数组内存区域。

static void Main(string[] args)
{
    const int N = 100000000;
    long ticks_start, ticks_end;

    ticks_start = DateTime.Now.Ticks;

    float[] a = { 10, 10, 10, 10 };
    float[] b = new float[4];

    for (int i = 0; i < N; i++)
        for (int j = 0; j < 4; j++)
            b[j] = a[j] + a[j];

    ticks_end = DateTime.Now.Ticks;
    Console.WriteLine(string.Join(", ", b));
    Console.WriteLine($"Done in {ticks_end - ticks_start} ticks");

    Console.WriteLine("Clearing...");
    Array.Clear(b, 0, b.Length);
    Console.WriteLine(string.Join(", ", b));


    ticks_start = DateTime.Now.Ticks;

    ReadOnlySpan<Vector4> v = MemoryMarshal.Cast<float, Vector4>(a);
    Span<Vector4> r = MemoryMarshal.Cast<float, Vector4>(b);
    for (int i = 0; i < N; i++)
    {
        r[0] = Vector4.Add(v[0], v[0]);
    }

    ticks_end = DateTime.Now.Ticks;
    Console.WriteLine(string.Join(", ", b));
    Console.WriteLine($"Done in {ticks_end - ticks_start} ticks");

    Console.ReadKey();
}

运行 发布版本。

20, 20, 20, 20
Done in 2172911 ticks
Clearing...
0, 0, 0, 0
20, 20, 20, 20
Done in 649113 ticks

看起来快了 3.3 倍。