c# - AVX2 SIMD XOR 在 .NET 中没有产生性能改进

标签 c# .net-core simd intrinsics .net-core-3.0

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

我有一些代码在循环中执行 4 个异或运算 - 下面是一个简化的示例(我不是在 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
        }
    }
}

Read64 方法如下所示:

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

我尝试将 4 行异或行替换为:

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
        }
    }
}

这确实给出了预期的结果 - 但它比仅乘以标量 慢 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
        }
    }
}

最佳答案

TL;DR AVX2 硬件内在函数使用不当导致生成非常低效的 SIMD 代码。

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

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

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

您正在使用的代码示例是一个简单的循环,以 quad unsigned quadword 步长修改数据,非常适合通过优化编译器进行自动矢量化。当 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;
    }
}

Godbolt Compiler Explorer result 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

Godbolt Compiler Explorer result 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

关于c# - AVX2 SIMD XOR 在 .NET 中没有产生性能改进,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56036938/

相关文章:

c# - EF Core 2.1 在分组后进行子查询和聚合时在本地进行评估

c# - 并非所有属性都在 WCF 调用中发送或接收

c# - Winforms (.NET Core 3) 是否支持所有 .NET Framework 控件

asp.net-core - 如何使用 HostBuilder 进行 WebJob?

c - 为什么这个 C 函数以二进制补码形式返回 int 值?

c++ - 如何使用 SSE 将 16 位整数除以 255?

c# - 在 C# 中比较 2 个 FileAttributes 必须始终返回 true

c# - 如何在不安装 Microsoft Lync 的情况下在我的业务应用程序中创建自定义 Lync 客户端?

c# - .Net Core Connection 目前有事务登记

matrix - 行大小大于向量宽度时的 SIMD 转置