【问题标题】:Vector<double> weak SIMD PerformanceVector<double> 弱 SIMD 性能
【发布时间】:2018-12-15 22:14:00
【问题描述】:

我正在优化一种算法,并且正在考虑将 Vector over double 用于乘法和累加操作。最接近的实现显然是 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)。

有什么想法吗? 谢谢大家!

【问题讨论】:

  • 基准测试失败,时间完全由抖动开销决定。很难衡量,这是非常快的代码。在它周围放置一个 for(;;) 循环以运行 10 次,以消除抖动开销并感受可变性。选择一个大于 24 的数字以查看任何真正的改进。顺便说一句,当前代码生成器中没有 FMA。
  • 我明白你的意思,改变了参数并获得了更好的结果,但就我而言,这可能不是正确的方法!
  • 请使用真正的基准测试框架,github.com/dotnet/BenchmarkDotNet 就像 Hans 提到的那样,只有当您摆脱开销时,测试才会有用。
  • 一有空闲时间,我就会尝试做 BenchmarkDotNet 的事情......
  • @harold 删除了他的答案,但重要的是您应该累积结果向量,而不是在内部循环内缩小为标量。并使用多个累加器来隐藏 FP 延迟。

标签: c# simd system.numerics


【解决方案1】:

使用 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

根据评论添加点积外循环的情况和点积的 ASm..

【讨论】:

  • 您仍在内部循环中使用Vector.Dot()。这很讨厌;使用向量累加器并在最后进行水平总和。
  • 阿格瑞德。关键是要向 OP 表明他的基准测试差了很多。将编辑并添加最佳案例。
  • 或者 OP 在 AMD CPU 上,dppd 比 Intel 更糟糕,假设这是 .Dot() 编译/JIT 到的。 (Ryzen 上每 3 个时钟一个,而 Haswell 上每个时钟 1 个:agner.org/optimize)。尽管除非使用多个累加器展开循环,否则 FP-add 延迟应该仍然是瓶颈。
  • 谢谢,帮了我很多忙!
猜你喜欢
  • 2018-08-24
  • 2015-01-17
  • 1970-01-01
  • 1970-01-01
  • 2012-02-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多