【问题标题】:C# Improve performance of SIMD Sum [closed]C#提高SIMD Sum的性能[关闭]
【发布时间】:2021-01-04 16:03:22
【问题描述】:

我正在编写一个 SIMD 库,并试图充分发挥性能。
我已经将array 就地转换为Span<Vector<int>>,而不是创建新对象。
目标数组很大(超过 1000 个元素)。
有没有更有效的方法对数组求和?
欢迎提出想法。

    public static int Sum(int[] array)
    {
        Vector<int> vSum = Vector<T>.Zero;
        int sum;
        int i;

        Span<Vector<int>> vsArray = MemoryMarshal.Cast<int, Vector<int>>(array);

        for (i = 0; i < vsArray.Length; i++)
        {
            vSum += vsArray[i];
        }

        sum = Vector.Dot(vSum, Vector<int>.One);

        i *= Vector<int>.Count;

        for (; i < array.Length; i++)
        {
            sum += array[i];
        }

        return sum;
    }

【问题讨论】:

  • 除非 C# 使用 1 优化点积中的乘法,否则效率不高。它在循环之外,所以它非常小,但仍然无缘无故地需要一个向量常数。 Fastest way to do horizontal SSE vector sum (or other reduction) 展示了 C intriniscs 的有效水平求和。
  • 除此之外,这是相当合理的。如果 C# 不为您执行此操作(使用多个矢量累加器)以隐藏延迟并允许吞吐量为 2 个矢量加载 + 如果数据在缓存中很热,则每个时钟添加的吞吐量可能需要手动展开循环。但是我没有 C# 开发设置来查看您实际上从中得到了什么。
  • 看看youtube.com/watch?v=mFZIj2y3Le0。也许有帮助。
  • 另外,这里是habr.com/en/post/467689

标签: c# performance simd


【解决方案1】:

你的代码很好。只能提高 4%,方法如下:

// Test result: only 4% win on my PC.
[MethodImpl( MethodImplOptions.AggressiveInlining )]
static int sumUnsafeAvx2( int[] array )
{
    unsafe
    {
        fixed( int* sourcePointer = array )
        {
            int* pointerEnd = sourcePointer + array.Length;
            int* pointerEndAligned = sourcePointer + ( array.Length - array.Length % 16 );
            Vector256<int> sumLow = Vector256<int>.Zero;
            Vector256<int> sumHigh = sumLow;
            int* pointer;
            for( pointer = sourcePointer; pointer < pointerEndAligned; pointer += 16 )
            {
                var a = Avx.LoadVector256( pointer );
                var b = Avx.LoadVector256( pointer + 8 );
                sumLow = Avx2.Add( sumLow, a );
                sumHigh = Avx2.Add( sumHigh, b );
            }
            sumLow = Avx2.Add( sumLow, sumHigh );
            Vector128<int> res4 = Sse2.Add( sumLow.GetLower(), sumLow.GetUpper() );
            res4 = Sse2.Add( res4, Sse2.Shuffle( res4, 0x4E ) );
            res4 = Sse2.Add( res4, Sse2.Shuffle( res4, 1 ) );
            int scalar = res4.ToScalar();
            for( ; pointer < pointerEnd; pointer++ )
                scalar += *pointer;
            return scalar;
        }
    }
}

Here's a complete test.

为了清楚起见,我不建议做我上面写的。不是为了 4% 的改进。不安全的代码是不安全的。你的版本在没有 AVX2 的情况下也能工作,如果有 AVX512 的话,我的优化版本会崩溃,即使硬件支持它也不会使用 AVX512。

【讨论】:

  • 如果元素计数 % 16 很高,您的代码也可能会花费更多时间进行标量清理。 128 位向量清理最多可以留下 0..3 个元素,同时将 2x 256 位向量减少到 128 个。(此外,您的标量清理循环可以写入一个单独的 0 初始化变量,允许一些 ILP如果编译器不为您进行转换,那么添加和负载都会受益。)
  • 请注意,您对代码进行基准测试的方式并不那么精确。如果您想以最精确的方式对代码进行基准测试,那么您可以使用 BenchmarkDotNet:github.com/dotnet/BenchmarkDotNet
  • @JanTamisKossen “不是那么精确”,为什么会这样?
  • @Soonts BenchmarkDotNet 可以以纳秒为单位测量时间,而秒表类无法做到这一点。此外,BenchmarkDotNet 可以显示多个测试的结果。
  • @JanTamisKossen 纳秒对于该测试并不重要,所测量的时间要长得多。秒表在后台使用 QueryPerformanceCounter,在现代 PC 上,该计时器的分辨率通常为 10MHz。
猜你喜欢
  • 1970-01-01
  • 2014-10-23
  • 1970-01-01
  • 1970-01-01
  • 2013-08-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多