Выравнивание и странное поведение SSE

c++ c intel sse simd

766 просмотра

1 ответ

669 Репутация автора

Я пытаюсь работать с SSE, и я столкнулся с некоторым странным поведением.

Я пишу простой код для сравнения двух строк с SSE Intrinsics, запускаю его, и он работает. Но позже я понимаю, что в моем коде один из указателей все еще не выровнен, но я использую _mm_load_si128инструкцию, которая требует указателя выровненного по 16-байтовой границе.

//Compare two different, not overlapping piece of memory
__attribute((target("avx"))) int is_equal(const void* src_1, const void* src_2, size_t size)
{
    //Skip tail for right alignment of pointer [head_1]
    const char* head_1 = (const char*)src_1;
    const char* head_2 = (const char*)src_2;
    size_t tail_n = 0;
    while (((uintptr_t)head_1 % 16) != 0 && tail_n < size)
    {                                
        if (*head_1 != *head_2)
            return 0;
        head_1++, head_2++, tail_n++;
    }

    //Vectorized part: check equality of memory with SSE4.1 instructions
    //src1 - aligned, src2 - NOT aligned
    const __m128i* src1 = (const __m128i*)head_1;
    const __m128i* src2 = (const __m128i*)head_2;
    const size_t n = (size - tail_n) / 32;    
    for (size_t i = 0; i < n; ++i, src1 += 2, src2 += 2)
    {
        printf("src1 align: %d, src2 align: %d\n", align(src1) % 16, align(src2) % 16);
        __m128i mm11 = _mm_load_si128(src1);
        __m128i mm12 = _mm_load_si128(src1 + 1);
        __m128i mm21 = _mm_load_si128(src2);
        __m128i mm22 = _mm_load_si128(src2 + 1);

        __m128i mm1 = _mm_xor_si128(mm11, mm21);
        __m128i mm2 = _mm_xor_si128(mm12, mm22);

        __m128i mm = _mm_or_si128(mm1, mm2);

        if (!_mm_testz_si128(mm, mm))
            return 0;
    }

    //Check tail with scalar instructions
    const size_t rem = (size - tail_n) % 32;
    const char* tail_1 = (const char*)src1;
    const char* tail_2 = (const char*)src2;
    for (size_t i = 0; i < rem; i++, tail_1++, tail_2++)
    {
        if (*tail_1 != *tail_2)
            return 0;   
    }
    return 1;
}

Я печатаю выравнивание двух указателей и одного из этих выравниваний, но второго - нет. И программа по-прежнему работает правильно и быстро.

Затем я создаю синтетический тест, как это:

//printChars128(...) function just print 16 byte values from __m128i
const __m128i* A = (const __m128i*)buf;
const __m128i* B = (const __m128i*)(buf + rand() % 15 + 1);
for (int i = 0; i < 5; i++, A++, B++)
{
    __m128i A1 = _mm_load_si128(A);
    __m128i B1 = _mm_load_si128(B);
    printChars128(A1);
    printChars128(B1);
}

И он вылетает, как мы и ожидали, на первой итерации при попытке загрузить указатель B.

Интересный факт, что если я переключусь targetна sse4.2то, моя реализация is_equalпотерпит крах.

Еще один интересный факт: если я попытаюсь выровнять второй указатель вместо первого (поэтому первый указатель не будет выровнен, второй выровнен), то is_equalпроизойдет сбой.

Итак, мой вопрос: «Почему is_equalфункция работает нормально только с первым указателем, если я включил avxгенерацию инструкций?»

UPD: это C++код. Я компилирую свой код с MinGW64/g++, gcc version 4.9.2Windows, x86.

Строка компиляции: g++.exe main.cpp -Wall -Wextra -std=c++11 -O2 -Wcast-align -Wcast-qual -o main.exe

Автор: Nikita Sivukhin Источник Размещён: 18.07.2016 06:16

Ответы (1)


5 плюса

164176 Репутация автора

Решение

TL: DR : загрузки из _mm_load_*встроенных функций могут быть сложены (во время компиляции) в операнды памяти для других инструкций. Версии векторных инструкций AVX не требуют выравнивания для операндов памяти , за исключением специально выровненных инструкций загрузки / сохранения, подобных vmovdqa.


В унаследованном SSE-кодировании векторных инструкций (например pxor xmm0, [src1]), 128-разрядные операнды памяти с невыровненным выравниванием будут ошибаться, за исключением специальных невыгруженных инструкций загрузки / сохранения (например, movdqu/ movups).

VEX-кодирование векторных инструкций (как vpxor xmm1, xmm0, [src1]) не вино с выровненной памятью, за исключением с выравниванием требуемых для инструкций загрузки / сохранения (например vmovdqa, или vmovntdq).


В _mm_loadu_si128сравнении с _mm_load_si128(и магазин / storeu) встроенные функции общения выравнивания гарантий компилятора, но не заставит его фактически испускают команду загрузки автономной. (Или что-нибудь вообще, если у него уже есть данные в регистре, как разыменование скалярного указателя).

Правило «как если» все еще применяется при оптимизации кода, который использует встроенные функции. Нагрузку можно сложить в операнд памяти для команды vector-ALU, которая ее использует, при условии, что это не представляет риска ошибки. Это выгодно по причинам плотности кода, а также благодаря меньшему количеству мопов, которые можно отследить в частях ЦП, благодаря микро-слиянию (см. Microarch.pdf от Agner Fog) . Этап оптимизации, который делает это, не разрешен -O0, поэтому неоптимизированная сборка вашего кода, вероятно, была бы ошибочной с невыровненным src1.

(С другой стороны, это означает, что он _mm_loadu_*может складываться в операнд памяти только с AVX, но не с SSE. Поэтому даже на процессорах, где movdquскорость такая же, как и movqdaпри выравнивании указателя, _mm_loaduможет ухудшить производительность, потому что movqdu xmm1, [rsi]/ pxor xmm0, xmm1is 2 uops для слитых доменов для интерфейс для выдачи, в то время pxor xmm0, [rsi]как только 1. И не нуждается в чистом регистре. См. также режимы Micro Fusion и адресации ).

Интерпретация правила «как будто» в этом случае заключается в том, что программа может не ошибаться в некоторых случаях, когда наивный перевод в asm не состоялся. (Или для того же кода, чтобы ошибка в неоптимизированной сборке, но не ошибка в оптимизированной сборке).

Это противоположно правилам для исключений с плавающей точкой, где сгенерированный компилятором код должен по-прежнему вызывать все исключения, которые произошли бы на абстрактной машине C. Это потому, что существуют четко определенные механизмы для обработки исключений FP, но не для обработки ошибок по умолчанию.


Обратите внимание, что поскольку хранилища не могут складываться в операнды памяти для инструкций ALU, встроенные функции store(не storeu) будут компилироваться в код, который выходит из строя с невыровненными указателями даже при компиляции для цели AVX.


Чтобы быть конкретным: рассмотрим этот фрагмент кода:

// aligned version:
y = ...;                         // assume it's in xmm1
x = _mm_load_si128(Aptr);        // Aligned pointer
res = _mm_or_si128(y, x);

// unaligned version: the same thing with _mm_loadu_si128(Uptr)

При нацеливании на SSE (код, который может работать на процессорах без поддержки AVX), выровненная версия может сложить нагрузку por xmm1, [Aptr], но не выровненная версия должна использовать
movdqu xmm0, [Uptr]/ por xmm0, xmm1. Выровненная версия тоже может это сделать, если yпосле ИЛИ все еще требуется старое значение .

При ориентации AVX ( gcc -mavxили gcc -march=sandybridgeили более поздней версии), все векторные инструкции , испускаемые ( в том числе 128 бит) будут использовать кодировку VEX. Таким образом, вы получаете разные asm от одной и той же _mm_...встроенной функции. Обе версии могут компилироваться в vpor xmm0, xmm1, [ptr]. (И неразрушающий признак с 3 операндами означает, что это действительно происходит за исключением случаев, когда загруженное исходное значение используется несколько раз).

Только один операнд в инструкциях ALU может быть операндом памяти , поэтому в вашем случае он должен быть загружен отдельно. Ваш код дает сбой, когда первый указатель не выровнен, но не заботится о выравнивании для второго, поэтому мы можем заключить, что gcc решил загрузить первый операнд vmovdqaи сложить второй, а не наоборот.

Вы можете увидеть, как это происходит на практике в вашем коде в проводнике компилятора Godbolt . К сожалению, gcc 4.9 (и 5.3) компилирует его в несколько неоптимальный код, который генерирует возвращаемое значение alи затем проверяет его, а не просто разветвляет флаги из vptest:( clang-3.8 делает работу значительно лучше.

.L36:
        add     rdi, 32
        add     rsi, 32
        cmp     rdi, rcx
        je      .L9
.L10:
        vmovdqa xmm0, XMMWORD PTR [rdi]           # first arg: loads that will fault on unaligned
        xor     eax, eax
        vpxor   xmm1, xmm0, XMMWORD PTR [rsi]     # second arg: loads that don't care about alignment
        vmovdqa xmm0, XMMWORD PTR [rdi+16]        # first arg
        vpxor   xmm0, xmm0, XMMWORD PTR [rsi+16]  # second arg
        vpor    xmm0, xmm1, xmm0
        vptest  xmm0, xmm0
        sete    al                                 # generate a boolean in a reg
        test    eax, eax
        jne     .L36                               # then test&branch on it.  /facepalm

Обратите внимание, что ваш is_equalесть memcmp. Я думаю, что memcmp glibc во многих случаях будет работать лучше, чем ваша реализация, так как он имеет рукописные версии asm для SSE4.1 и других, которые обрабатывают различные случаи смещения буферов относительно друг друга. (например, один выровненный, другой - нет.) Обратите внимание, что код glibc LGPLed, поэтому вы не сможете просто скопировать его. Если ваш вариант использования имеет меньшие буферы, которые обычно выровнены, ваша реализация, вероятно, хороша. Не нужно VZEROUPPER перед вызовом его из другого кода AVX, это тоже хорошо.

Сгенерированный компилятором байтовый цикл для очистки в конце определенно неоптимален. Если размер больше 16 байтов, выполните загрузку без выравнивания, которая заканчивается на последнем байте каждого источника. Неважно, что вы сравнили некоторые байты, которые вы уже проверили.

В любом случае, обязательно сравните ваш код с системой memcmp. Помимо реализации библиотеки, gcc знает, что делает memcmp, и имеет собственное встроенное определение, для которого он может встроить код.

Автор: Peter Cordes Размещён: 19.07.2016 03:10
32x32