AVX2 SIMD XOR在.NET中未产生性能改进

Coc*_*lla 6 c# simd intrinsics .net-core .net-core-3.0

我正在使用System.Runtime.Intrinsics命名空间中的.NET Core 3.0对硬件内在函数的新支持。

我有一些代码可以在一个循环中执行4个XOR操作-下面是一个简化的示例(我没有在IDE中编写此代码,因此请忽略任何语法错误:

private static unsafe ulong WyHashCore(byte[] array)
{
    fixed (byte* pData = array)
    {
        byte* ptr = pData;

        // Consume 32-byte chunks
        for (int i = 0; i < array.Length; i += 32)
        {
            ulong a = Read64(ptr, i);
            ulong b = Read64(ptr, i + 8);
            ulong c = Read64(ptr, i + 16);
            ulong d = Read64(ptr, i + 24);

            // XOR them with some constants
            ulong xor1 = a ^ SOME_CONSTANT1;
            ulong xor2 = b ^ SOME_CONSTANT2;
            ulong xor3 = c ^ SOME_CONSTANT3;
            ulong xor4 = d ^ SOME_CONSTANT4;

            // Use the resulting values
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Read64方法如下所示:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe ulong Read64(byte* ptr, int start)
    => *(ulong*)(ptr + start);
Run Code Online (Sandbox Code Playgroud)

我尝试用以下方式替换4个XOR行:

byte[] array; // An array from elsewhere

private static unsafe ulong WyHashCore(byte[] array)
{
    var bVector = Vector256.Create(SOME_CONSTANT1, SOME_CONSTANT2, SOME_CONSTANT3, SOME_CONSTANT4);

    fixed (byte* pData = array)
    {
        byte* ptr = pData;

        // Consume 32-byte chunks
        for (int i = 0; i < array.Length; i += 32)
        {
            ulong a = Read64(ptr, i);
            ulong b = Read64(ptr, i + 8);
            ulong c = Read64(ptr, i + 16);
            ulong d = Read64(ptr, i + 24);

            // Create a 256-bit vector from the 4 64-bit integers
            var aVector = Vector256.Create(a, b, c, d);

            // XOR the 2 vectors
            var res = Avx2.Xor(aVector, bVector);

            // Get the resulting values out of the result vector
            ulong xor1 = res.GetElement(0);
            ulong xor2 = res.GetElement(1);
            ulong xor3 = res.GetElement(2);
            ulong xor4 = res.GetElement(3);

            // Use the resulting values
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这确实可以达到预期的结果-但比只乘标量要慢 15倍!

我在某个地方出错或滥用SIMD吗?

**更新**我已经更新了代码,以使用“正确”的方式将数据加载到向量或从向量中卸载数据,现在它的速度比标量代码快了3.75倍!

byte[] array; // An array from elsewhere
private static readonly Vector256<ulong> PrimeVector = Vector256.Create(SOME_CONSTANT1, SOME_CONSTANT2, SOME_CONSTANT3, SOME_CONSTANT4);

private static unsafe ulong WyHashCore(byte[] array)
{
    // Create space on the stack to hold XOR results
    var xorResult = stackalloc ulong[4];

    fixed (byte* pData = array)
    {
        byte* ptr = pData;

        // Consume 32-byte chunks
        for (int i = 0; i < array.Length; i += 32)
        {
            // Create a 256-bit vector from the 4 64-bit integers
            var vector = Avx.LoadVector256((ulong*)(ptr + i));

            // XOR the 2 vectors
            var res = Avx2.Xor(vector, PrimeVector);

            // Store the resulting vector in memory
            Avx2.Store(xorResult, res);

            // Get the resulting values out of the result vector
            var xor1 = *xorResult;
            var xor2 = *(xorResult + 1);
            var xor3 = *(xorResult + 2);
            var xor4 = *(xorResult + 3);

            // Use the resulting values
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Jac*_*ski 4

TL;DR AVX2 硬件内部函数使用不正确,导致生成效率非常低的 SIMD 代码。

错误在于指令在缓冲区中加载、操作和存储数据的方式。该操作应使用 AVX/AVX2 Avx2.X 或具有内存的内部函数来执行,这将使加载时间加快 4 倍并返回 Vector256。另一方面,这将使对 Vector256.Create 的调用变得多余,并进一步加快执行速度。最后,应使用 Avx2.Store() 内部函数将数据存储在数组中。这又会将代码速度提高大约 4 倍。

应该应用的最后一个优化是利用 CPU 指令级并行性。通常,SIMD 指令在预定义数量的 CPU 周期内执行,延迟可能大于 1 个 CPU 周期。这些参数是特定于 CPU 的,可以在以下位置找到:

由于所有可以应用的优化都非常复杂,我稍后会在更长的文章中解释它们,但总的来说,与您正在处理的问题的基本情况相比,我预计由于矢量化,速度会提高 4 倍。

您使用的代码示例是一个简单的循环,以四无符号四字步骤修改数据,并且是通过优化编译器进行自动向量化的完美候选者。当 GCC 9.1 使用选项-O3 -march=haswell优化相同的 C++ 循环时,生成的机器代码显示应用于循环的所有标准优化:

#include <cstdint>
void hash(uint64_t* buffer, uint64_t length) {

    uint64_t* pBuffer = buffer;
    const uint64_t CONST1 = 0x6753ul;
    const uint64_t CONST2 = 0x7753ul;
    const uint64_t CONST3 = 0x8753ul;
    const uint64_t CONST4 = 0x9753ul;

    for(uint64_t i = 0; i < length; i += 4)
    {
        *pBuffer ^= CONST1;
        *(pBuffer + 1) ^= CONST2;
        *(pBuffer + 2) ^= CONST3;
        *(pBuffer + 3) ^= CONST4;
    }
}
Run Code Online (Sandbox Code Playgroud)

Godbolt 编译器资源管理器结果 GCC 9.1

    test    rsi, rsi
    je      .L11
    cmp     rsi, -4
    ja      .L6
    lea     rdx, [rsi-1]
    vmovdqa ymm1, YMMWORD PTR .LC0[rip]
    xor     eax, eax
    shr     rdx, 2
    inc     rdx
.L5:
    vpxor   ymm0, ymm1, YMMWORD PTR [rdi]
    inc     rax
    add     rdi, 32
    vmovdqu YMMWORD PTR [rdi-32], ymm0
    cmp     rax, rdx
    jb      .L5
    vzeroupper
.L11:
    ret
.L6:
    vmovdqa ymm1, YMMWORD PTR .LC0[rip]
    xor     eax, eax
.L3:
    vpxor   ymm0, ymm1, YMMWORD PTR [rdi]
    add     rax, 4
    add     rdi, 32
    vmovdqu YMMWORD PTR [rdi-32], ymm0
    cmp     rsi, rax
    ja      .L3
    vzeroupper
    jmp     .L11
.LC0:
    .quad   26451
    .quad   30547
    .quad   34643
    .quad   38739
Run Code Online (Sandbox Code Playgroud)

Godbolt 编译器资源管理器结果 Clang 8.0

 .LCPI0_0:
    .quad   26451                   # 0x6753
    .quad   30547                   # 0x7753
    .quad   34643                   # 0x8753
    .quad   38739                   # 0x9753
 hash(unsigned long*, unsigned long):                             # @hash(unsigned long*, unsigned long)
    test    rsi, rsi
    je      .LBB0_3
    xor     eax, eax
    vmovaps ymm0, ymmword ptr [rip + .LCPI0_0] # ymm0 = [26451,30547,34643,38739]
 .LBB0_2:                                # =>This Inner Loop Header: Depth=1
    vxorps  ymm1, ymm0, ymmword ptr [rdi + 8*rax]
    vmovups ymmword ptr [rdi + 8*rax], ymm1
    add     rax, 4
    cmp     rax, rsi
    jb      .LBB0_2
 .LBB0_3:
    vzeroupper
    ret
Run Code Online (Sandbox Code Playgroud)

  • @Peter-Cordes,您指出的答案确实有助于更好地理解为什么 SIMD 增益可能不符合预期,谢谢! (2认同)