AVX2 SIMD XOR не дает улучшения производительности в .NET - PullRequest
5 голосов
/ 08 мая 2019

Я играю с новой поддержкой .NET Core 3.0 для аппаратных встроенных функций в пространстве имен System.Runtime.Intrinsics.

У меня есть код, где я выполняю 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
        }
    }
}

Метод Read64 выглядит следующим образом:

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

Я попытался заменить 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
        }
    }
}

Это дает ожидаемые результаты - но это в 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
        }
    }
}

1 Ответ

2 голосов
/ 08 мая 2019

TL; DR Встроенные функции AVX2 HW используются неправильно, что приводит к генерации очень неэффективного кода SIMD.

Ошибка заключается в том, что инструкции загружают, обрабатывают и хранят данныев буфере.Операция должна быть выполнена с использованием встроенных в память AVX / AVX2 Avx2.Xor, что ускорит время загрузки в 4 раза и вернет Vector256.Это, с другой стороны, вызовет Vector256.Create избыточный и ускорит выполнение.Наконец, данные должны храниться в массиве с помощью встроенного Avx2.Store ().Это снова ускорит код примерно в 4 раза.

Последняя оптимизация, которая должна быть применена, - это использование параллелизма на уровне команд ЦП.Обычно инструкции SIMD выполняются в течение заранее определенного числа циклов ЦП с задержкой, которая может превышать 1 цикл ЦП.Эти параметры зависят от процессора и могут быть найдены в:

Как и все оптимизации, которыемогут быть довольно сложными, я объясню их в более длинной записи чуть позже, но в целом я ожидаю ускорения до 4x из-за векторизации по сравнению с базовым сценарием для проблемы, над которой вы работаете.

КодПример, который вы используете, представляет собой простой цикл изменения данных в шагах с четырьмя беззнаковыми четырьмя словами и является идеальным кандидатом для автовекторизации за счет оптимизации компиляторов.Когда идентичный цикл C ++ оптимизирован с помощью GCC 9.1 с параметрами -O3 -march = haswell , результирующий машинный код показывает все стандартные оптимизации, примененные к циклу:

#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Результат проводника 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 Результат проводника компилятора 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
...