С каждым новым поколением процессоров Intel появляются новые и все более сложные векторные инструкции. Хотя длина вектора (512 бит) в ближайшее время расти не будет, появятся новые типы данных и виды инструкций. Например, кто сможет с первого взгляда понять, что делает такой интринсик (и соответствующая ему инструкция процессора)?

Bitwise ternary logic that provides the capability to implement any three-operand binary function; the specific binary function is specified by value in imm8.

__m512i _mm512_mask_ternarylogic_epi32 (__m512i src, __mmask8 k, __m512i a, __m512i b, int imm8)
FOR j := 0 to 15
    i := j*32
    IF k[j]
        FOR h := 0 to 31
            index[2:0] := (src[i+h] << 2) OR (a[i+h] << 1) OR b[i+h]
            dst[i+h]   := imm8[index[2:0]]
        ENDFOR
    ELSE
        dst[i+31:i] := src[i+31:i]
    FI
ENDFOR
dst[MAX:512] := 0

ОК, допустим, мы разобрались, как она работает. Следующий уровень сложности — отладка кода, интенсивно использующего такие интринсики.

Те, кто регулярно пользуются интринсиками, знают такой очень полезный сайт — Intel intrinsics guide. Если внимательно посмотреть, как он устроен, то легко заметить, что javascript фронтенд скачивает файл data-3.x.x.xml, в котором подробно описаны все интринсики, с кодом, похожим на Matlab. (Например, тот, что я скопировал в заголовке поста.)

Но когда мы используем интринсики для ускорения кода, мы пишем не на Matlab, а на С и С++! Три месяца назад один клиент спросил меня, существует ли реализация векторных интринсиков на С для отладки, и я решил написать парсер, который транслирует код из Intrinsics Guide в С. Получается библиотека, которая реализует почти все интринсики так, что пошаговым отладчиком можно заходить внутрь (или добавить отладочных printf).

Например, операция из заголовка поста превращается в

for (int j = 0; j <= 15; j++) {
  if (k & (1 << j)) {
    for (int h = 0; h <= 31; h++) {
      int index =  ((((src_vec[j] & (1 << h)) >> h) << 2) |
                   (((a_vec[j] & (1 << h)) >> h) << 1) |
                   ((b_vec[j] & (1 << h)) >> h)) & 0x7;
      dst_vec[j] = (dst_vec[j] & ~(1 << h)) |
                   ((((imm8 & (1 << index)) >> index)) << h);
    }
  } else {
    dst_vec[j] = src_vec[j];
  }
}

Правда, так гораздо понятнее? Не очень? Ну, это я просто сложную функцию выбрал для примера. Обычно, когда отлаживаешь код с интринсиками, (например, DSP) приходится держать в голове как алгоритм, так и особенности каждой инструкции. Учитывая, что инструкции работают с длинными векторами, а DSP алгоритмы часто еще и основаны на серьезной математике, моя голова не справляется — кратковременной памяти и концентрации не хватает. Подозреваю, что я не одинок — несколько раз я даже думал, что нашел баг в инструкции. Потом, разумеется, каждый раз оказывалось, что ошибался я, и открыть новый FDIV bug не получилось. Но если бы я мог в тех случаях пошагово отлаживать внутри инструкции, я сразу же понял бы, при каких условиях в компоненте моего вектора появляется значение, которое я не ожидал.

Клиенты мне говорили, что используют эту библиотеку для того, чтобы отлаживать отдельные функции с AVX-512 интринсиками на лэптопе, который поддерживает только AVX2. Конечно, для этого гораздо лучше подходит Intel SDE — потому, что он предельно аккуратно имитирует все наборы инструкций. У меня есть набор юнит тестов (тоже автоматически сгенерированных), которые для каждого интринсика из библиотеки сравнивают результат его работы с результатом исполнения соответствующей ассемблерной инструкции. Как и положено юнит тестам, большинство работает как надо. Но некоторые отладочные интринсики с плавающей точкой (и двойной, и одинарной точности) не всегда работают на 100% корректно. Я бы сказал, что иногда получается эдакий -ffast-math. А еще есть разные механизмы округления! В IEE754 много тонкостей…

Есть еще одна важная особенность использования immintrin debug вместо SDE (что я всячески не одобряю, но помешать этому не могу). Если компилировать gcc или clang с опцией, например, -march=nehalem, то gcc и clang возвращают из функций 512-битные вектора на стеке, а ICC все равно возвращает их в ZMM0. Так что компилятор Intel в этом режиме использовать не получится. А еще у gcc есть полезная опция -Og, которая помогает при отладке, в том числе с immintrin debug.

Существует несколько интринсиков, главное действие которых — изменение содержимого регистра, например, или флагов. Такие инструкции я не реализовал. Ну и пока мой парсер не совсем готов, реализация еще примерно 10% интринсиков еще отсутствует.

Использовать immintrin debug очень просто — исходники менять не надо, но придется добавить условную компиляцию, чтобы включать immintrin_dbg.h вместо immintrin.h в случае отладочной сборки.

Скачать можно на гитхабе.

Комментарии (0)