向量类型<double> 弱 SIMD 性能

3

我正在优化一个算法,考虑在乘加运算中使用向量而不是双精度浮点数。最接近实现的方法显然是使用Vector.dot(v1, v2); 但是,为什么我的代码运行得如此缓慢呢?

namespace ConsoleApp1 {
    class Program {
        public static double SIMDMultAccumulate(double[] inp1, double[] inp2) {

            var simdLength = Vector<double>.Count;
            var returnDouble = 0d;

            // Find the max and min for each of Vector<ushort>.Count sub-arrays 
            var i = 0;
            for (; i <= inp1.Length - simdLength; i += simdLength) {
                var va = new Vector<double>(inp1, i);
                var vb = new Vector<double>(inp2, i);
                returnDouble += Vector.Dot(va, vb);
            }

            // Process any remaining elements
            for (; i < inp1.Length; ++i) {
                var va = new Vector<double>(inp1, i);
                var vb = new Vector<double>(inp2, i);
                returnDouble += Vector.Dot(va, vb);
            }

            return returnDouble;
        }


        public static double NonSIMDMultAccumulate(double[] inp1, double[] inp2) {
            var returnDouble = 0d;

            for (int i = 0; i < inp1.Length; i++) {
                returnDouble += inp1[i] * inp2[i];
            }

            return returnDouble;
        }

        static void Main(string[] args) {
            Console.WriteLine("Is hardware accelerated: " + Vector.IsHardwareAccelerated);

            const int size = 24;
            var inp1 = new double[size];
            var inp2 = new double[size];

            var random = new Random();
            for (var i = 0; i < inp1.Length; i++) {
                inp1[i] = random.NextDouble();
                inp2[i] = random.NextDouble();
            }

            var sumSafe = 0d;
            var sumFast = 0d;

            var sw = Stopwatch.StartNew();
            for (var i = 0; i < 10; i++) {
                sumSafe =  NonSIMDMultAccumulate(inp1, inp2);
            }
            Console.WriteLine("{0} Ticks", sw.Elapsed.Ticks);

            sw.Restart();
            for (var i = 0; i < 10; i++) {
                sumFast = SIMDMultAccumulate(inp1, inp2);
            }
            Console.WriteLine("{0} Ticks", sw.Elapsed.Ticks);

//            Assert.AreEqual(sumSafe, sumFast, 0.00000001);
        }
    }

}

SIMD版本与非SIMD版本相比,需要约70%的更多时钟周期。我正在运行Haswell架构,在我看来,应该实现FMA3!(发布版本,x64优先)。

有什么建议吗? 谢谢大家!


1
基准测试失败,时间完全被JIT开销所支配。很难测量,这是极快的代码。在其周围放置一个for(;;)循环以运行它10次,以消除JIT开销并感受其变化。选择比24更大的数字以看到任何真正的改进。当前代码生成器中没有FMA。 - Hans Passant
我理解你的观点,调整了参数后结果确实有所改善,但对于我的情况来说可能不是最佳方法! - Styp
3
请使用真正的基准测试框架,例如 https://github.com/dotnet/BenchmarkDotNet。就像 Hans 提到的那样,只有消除开销之后,测试才会变得有用。 - Lex Li
1
我会尽快尝试进行BenchmarkDotNet的测试,只要我有一些空闲时间... - Styp
1
@ Harold 删除了他的答案,但是他强调了一个重要观点,即应该累积结果向量而不是在内部循环中缩小为标量。并使用多个累加器隐藏FP延迟。 - Peter Cordes
这个实现是否正确?我不太了解C#,但是你构造new Vector<double>(inp1, i);的方式在主循环和剩余循环中都一样,看起来不对。另外,C#是否会优化掉Vector的构造? - chtz
1个回答

3

使用BechmarkDotNet时,假设输入数组的长度(ITEMS = 10000)是Vector.Count的倍数,在使用SIMD Vector时可以获得近乎双倍的性能:

    [Benchmark(Baseline = true)]
    public double DotDouble()
    {
        double returnVal = 0.0;
        for(int i = 0; i < ITEMS; i++)
        {
            returnVal += doubleArray[i] * doubleArray2[i];
        }
        return returnVal;
    }

    [Benchmark]
    public double DotDoubleVectorNaive()
    {
        double returnVal = 0.0;
        for(int i = 0; i < ITEMS; i += doubleSlots)
        {
           returnVal += Vector.Dot(new Vector<double>(doubleArray, i), new Vector<double>(doubleArray2, i));
        }
        return returnVal;  
    }

    [Benchmark]
    public double DotDoubleVectorBetter()
    {
        Vector<double> sumVect = Vector<double>.Zero;
        for (int i = 0; i < ITEMS; i += doubleSlots)
        {
            sumVect += new Vector<double>(doubleArray, i) * new Vector<double>(doubleArray2, i);
        }
        return Vector.Dot(sumVect, Vector<double>.One);
    }

    BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
    Intel Core i7-4500U CPU 1.80GHz (Haswell), 1 CPU, 4 logical and 2 physical cores
    Frequency=1753758 Hz, Resolution=570.2041 ns, Timer=TSC
    .NET Core SDK=2.1.300
      [Host]     : .NET Core 2.1.0 (CoreCLR 4.6.26515.07, CoreFX 4.6.26515.06), 64bit RyuJIT
      DefaultJob : .NET Core 2.1.0 (CoreCLR 4.6.26515.07, CoreFX 4.6.26515.06), 64bit RyuJIT


                Method |      Mean |     Error |    StdDev | Scaled |
---------------------- |----------:|----------:|----------:|-------:|
             DotDouble | 10.341 us | 0.0902 us | 0.0844 us |   1.00 |
  DotDoubleVectorNaive |  5.907 us | 0.0206 us | 0.0183 us |   0.57 |
 DotDoubleVectorBetter |  4.825 us | 0.0197 us | 0.0184 us |   0.47 |

为了完整性,RiuJIT将在Haswell上编译Vector.Dot乘积:

vmulpd  ymm0,ymm0,ymm1            
vhaddpd ymm0,ymm0,ymm0    
vextractf128 xmm2,ymm0,1                
vaddpd  xmm0,xmm0,xmm2              
vaddsd  xmm6,xmm6,xmm0

根据评论和点积的情况,编辑并在循环外部添加了Dot product的案例。同时提供了用于点积的汇编代码。


你仍然在内部循环中使用 Vector.Dot()。那很糟糕;使用向量累加器,并在最后进行水平求和。 - Peter Cordes
2
同意。重点是要向OP表明他的基准测试可能相差很大。将进行编辑并添加最佳情况。 - C. Gonzalez
或许原帖作者使用的是AMD CPU,其中dppd比在Intel上更糟糕,假设.Dot()编译/JIT到了那里。(在Ryzen上每3个时钟周期执行一次,而在您的Haswell上每个时钟周期执行一次:http://agner.org/optimize/)。尽管如此,除非循环展开并使用多个累加器,否则FP-add延迟仍应该是瓶颈。 - Peter Cordes
谢谢,帮了我很多! - Styp

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