C# Vector<double>.CopyTo是否比非SIMD版本快?

5

更新:之前提到的 Span 问题已在 .net core 2.1 发布版中得到解决(目前处于预览状态)。这实际上使 Span 向量比数组向量更快...

注意:我在“Intel Xeon E5-1660 v4”上进行了测试,CPU-Z告诉我它支持“MMX、SSE、SSE2、SSE3、SSSE3、SSE4.1、SSE4.2、EM64T、VT-x、AES、AVX、AVX2、FMA3、RSX”指令,所以应该没问题...

在回答一个基于向量的问题后,我想尝试实现一些 BLAS 函数。我发现像点积这样的读取/求和函数效果很好,但是当我写回到数组时效果不佳 - 比非 SIMD 好一点,但几乎没有改善。

那么我是做错了什么,还是 JIT 需要更多工作?

例如(假设 x.Length = y.Length,不为空等等):

public static void daxpy(double alpha, double[] x, double[] y)
{
    for (var i = 0; i < x.Length; ++i)
        y[i] = y[i] + x[i] * alpha;
}

以矢量形式表示为:
public static void daxpy(double alpha, double[] x, double[] y)
{
    var i = 0;
    if (Vector.IsHardwareAccelerated)
    {
        var length = x.Length + 1 - Vector<double>.Count;
        for (; i < length; i += Vector<double>.Count)
        {
            var valpha = new Vector<double>(alpha);
            var vx = new Vector<double>(x, i);
            var vy = new Vector<double>(y, i);
            (vy + vx * valpha).CopyTo(y, i);
        }
    }
    for (; i < x.Length; ++i)
        y[i] = y[i] + x[i] * alpha;
}

今天在.NET Core 2.0中尝试使用Span(包括naive和Vector格式)。

public static void daxpy(double alpha, Span<double> x, Span<double> y)
{
    for (var i = 0; i < x.Length; ++i)
        y[i] += x[i] * alpha;
}

和向量
public static void daxpy(double alpha, Span<double> x, Span<double> y)
{
    if (Vector.IsHardwareAccelerated)
    {
        var vx = x.NonPortableCast<double, Vector<double>>();
        var vy = y.NonPortableCast<double, Vector<double>>();

        var valpha = new Vector<double>(alpha);
        for (var i = 0; i < vx.Length; ++i)
            vy[i] += vx[i] * valpha;

        x = x.Slice(Vector<double>.Count * vx.Length);
        y = y.Slice(Vector<double>.Count * vy.Length);
    }

    for (var i = 0; i < x.Length; ++i)
        y[i] += x[i] * alpha;
}

所以所有这些的相对时间如下:
Naive       1.0
Vector      0.8
Span Naive  2.5 ==> Update: Span Naive  1.1
Span Vector 0.9 ==> Update: Span Vector 0.6

我做错了什么吗?我想不出更简单的例子,所以我认为没有问题。

你有查看向量化版本生成的 IL 吗? - Tigran
@Tigran,IL中没有什么奇怪的地方,恐怕我对汇编不够熟悉,无法解密哪些内容应该或不应该在那里... - Paul Westcott
免责声明:我以前从未使用过 Span,但是在阅读文档时,我注意到它是一个值类型。在您的情况下,您将其按值传递到函数中,该函数会在 Span<T> 类型上调用副本。 - Tigran
@PaulWestcott 啊,我错了。你的问题标题写着 Vector<double> - 这是一个STL类型,而不是.NET。 - Dai
2
@Tigran IL在这里几乎没有什么作用;JIT才是实现所有魔法的关键。 - Marc Gravell
显示剩余6条评论
1个回答

1

您可能更倾向于使用2.1而不是2.0进行测试; 在我的笔记本电脑上(与我的台式机相比SIMD性能较差),我得到了以下结果:

daxpy_naive x10000: 144ms
daxpy_arr_vector x10000: 77ms
daxpy_span x10000: 173ms
daxpy_vector x10000: 67ms
daxpy_vector_no_slice x10000: 67ms

using code:

using System;
using System.Diagnostics;
using System.Numerics;
class Program
{
    static void Main(string[] args)
    {
        double alpha = 0.5;
        double[] x = new double[16 * 1024], y = new double[x.Length];
        var rand = new Random(12345);
        for (int i = 0; i < x.Length; i++)
            x[i] = rand.NextDouble();

        RunAll(alpha, x, y, 1, false);
        RunAll(alpha, x, y, 10000, true);
    }

    private static void RunAll(double alpha, double[] x, double[] y, int loop, bool log)
    {
        GC.Collect(GC.MaxGeneration);
        GC.WaitForPendingFinalizers();

        var watch = Stopwatch.StartNew();
        for(int i = 0; i < loop; i++)
        {
            daxpy_naive(alpha, x, y);
        }
        watch.Stop();
        if (log) Console.WriteLine($"{nameof(daxpy_naive)} x{loop}: {watch.ElapsedMilliseconds}ms");

        watch = Stopwatch.StartNew();
        for (int i = 0; i < loop; i++)
        {
            daxpy_arr_vector(alpha, x, y);
        }
        watch.Stop();
        if (log) Console.WriteLine($"{nameof(daxpy_arr_vector)} x{loop}: {watch.ElapsedMilliseconds}ms");


        watch = Stopwatch.StartNew();
        for (int i = 0; i < loop; i++)
        {
            daxpy_span(alpha, x, y);
        }
        watch.Stop();
        if (log) Console.WriteLine($"{nameof(daxpy_span)} x{loop}: {watch.ElapsedMilliseconds}ms");

        watch = Stopwatch.StartNew();
        for (int i = 0; i < loop; i++)
        {
            daxpy_vector(alpha, x, y);
        }
        watch.Stop();
        if (log) Console.WriteLine($"{nameof(daxpy_vector)} x{loop}: {watch.ElapsedMilliseconds}ms");

        watch = Stopwatch.StartNew();
        for (int i = 0; i < loop; i++)
        {
            daxpy_vector_no_slice(alpha, x, y);
        }
        watch.Stop();
        if (log) Console.WriteLine($"{nameof(daxpy_vector_no_slice)} x{loop}: {watch.ElapsedMilliseconds}ms");
    }

    public static void daxpy_naive(double alpha, double[] x, double[] y)
    {
        for (var i = 0; i < x.Length; ++i)
            y[i] = y[i] + x[i] * alpha;
    }

    public static void daxpy_arr_vector(double alpha, double[] x, double[] y)
    {
        var i = 0;
        if (Vector.IsHardwareAccelerated)
        {
            var length = x.Length + 1 - Vector<double>.Count;
            for (; i < length; i += Vector<double>.Count)
            {
                var valpha = new Vector<double>(alpha);
                var vx = new Vector<double>(x, i);
                var vy = new Vector<double>(y, i);
                (vy + vx * valpha).CopyTo(y, i);
            }
        }
        for (; i < x.Length; ++i)
            y[i] = y[i] + x[i] * alpha;
    }
    public static void daxpy_span(double alpha, Span<double> x, Span<double> y)
    {
        for (var i = 0; i < x.Length; ++i)
            y[i] += x[i] * alpha;
    }

    public static void daxpy_vector(double alpha, Span<double> x, Span<double> y)
    {
        if (Vector.IsHardwareAccelerated)
        {
            var vx = x.NonPortableCast<double, Vector<double>>();
            var vy = y.NonPortableCast<double, Vector<double>>();

            var valpha = new Vector<double>(alpha);
            for (var i = 0; i < vx.Length; ++i)
                vy[i] += vx[i] * valpha;

            x = x.Slice(Vector<double>.Count * vx.Length);
            y = y.Slice(Vector<double>.Count * vy.Length);
        }

        for (var i = 0; i < x.Length; ++i)
            y[i] += x[i] * alpha;
    }

    public static void daxpy_vector_no_slice(double alpha, Span<double> x, Span<double> y)
    {
        int i = 0;
        if (Vector.IsHardwareAccelerated)
        {
            var vx = x.NonPortableCast<double, Vector<double>>();
            var vy = y.NonPortableCast<double, Vector<double>>();

            var valpha = new Vector<double>(alpha);
            for (i = 0; i < vx.Length; ++i)
                vy[i] += vx[i] * valpha;

            i = Vector<double>.Count * vx.Length;
        }

        for (; i < x.Length; ++i)
            y[i] += x[i] * alpha;
    }
}

这段内容涉及编程,使用的命令分别是dotnet build -c Releasedotnet run -c Release,并且dotnet --version显示版本号为"2.2.0-preview1-008000"(之前的一份“每日构建”)。

在我的桌面电脑上,我预计性能差距会更大。


你如何解释Span版本比朴素版本略慢的现象?这可能是因为在Span情况下,元素进行了原地累加,而Jitter可能会以不同的方式进行优化。除此之外,从提供的代码中,我没有看到任何其他明显的区别。这比“速度不足”更让我感到困惑。 - Tigran
好的,运行在“.NET Core 2.1.101”上(来自我的原始发布的Xeon),我得到了[122ms,83ms,244ms,91ms],这与我的原始结果相差不大。我会尝试在其他机器上运行...也许2.2版本会稍微改善一些...但是我认为它们应该是4倍精度,所以即使您有更好的相对结果,它们仍然很糟糕(作为参考,我正在与OpenBLAS实现进行比较 - 虽然我无法接近它,但它的运行时间约为Naive版本的1/5,因此我希望能达到1/3至1/4...) - Paul Westcott
哦,我注意到我的代码有一点不一致,就是在某些地方我写成了“y[i] = y[i] +”,而在其他地方则写成了“y[i] + =”。如果我在Naive Span中将后者换成前者,那么时间会增加两倍(现在比Naive慢4倍),但是如果我在非Span Naive中将前者换成后者,则时间会增加10-20%(但也许这只是运气?)。向量Span保持不变。 - Paul Westcott
实际上,关于非 Span naive 和 += 的更改的先前评论仅适用于 32 位构建(我只是在那里玩耍...)。在 64 位中没有注意到任何区别。但是,Span 版本的差异仍然存在。 - Paul Westcott
在我的桌面电脑上(虽然有点老,但是CPU支持SIMD),我可以重现OP的结果。Naive是最快的,向量稍微慢一些,跨度版本则是它们中最慢的(比Naive和Vector慢2倍)。这是在.NET 2.1.2上的情况。 - Evk
@PaulWestcott 我会把所有的信息都添加到你的问题底部。 - Tigran

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接