Конвенция для отображения векторных регистров

Есть ли соглашение для отображения/записи больших регистров, например, доступных в наборе инструкций Intel AVX?

Например, если у вас есть 1 наименее значимого байта и 20 в самом значительном байте и 0 в другом месте в регистре xmm, для байт-мудрый дисплей является следующим предпочтительным (little-endian):

[1, 0, 0, 0, ..., 0, 20]

или является предпочтительным:

[20, 0, 0, 0, ..., 0, 1]

Аналогично, при отображении таких регистров, состоящих из более крупных элементов данных, применяется одно и то же правило? Например, чтобы отобразить регистр как DWORD, я предполагаю, что каждый DWORD все еще записан обычным (по-бинарному) образом, но каков порядок DWORDS:

[0x1, 0x0, ..., 0x14]

против

[0x14, 0x0, ..., 0x1]

Обсуждение

Я думаю, что два наиболее перспективных ответа - это просто "LSE 1 сначала" (т.е. первый вывод в приведенных выше примерах) или "MSE first" (второй вывод). Ни один из них не зависит от сущности платформы, так как действительно один раз в регистре данные, как правило, независимы от конца (например, операции в регистре GP или long или int или что-то другое в C не зависят от сущности). Endianness появляется в интерфейсе register ↔ памяти, и здесь я запрашиваю данные уже в регистре.

Возможно, существуют и другие ответы, такие как вывод, который зависит от сущности (и ответ Пол R может быть одним, но я не могу сказать).

LSE First

Одним из преимуществ LSE-first, по-видимому, является особенно байт-выход: часто байты нумеруются от 0 до N, при этом LSB равен нулю 2 поэтому вывод LSB-first выводит его с увеличением индексов, так же, как вы выведете массив байтов размера N.

Он также хорош в маленьких endian-архитектурах, поскольку выход затем соответствует представлению в памяти одного и того же вектора, хранящегося в памяти.

MSE First

Основным преимуществом здесь является то, что выход для более мелких элементов находится в том же порядке, что и для больших размеров (только с разной группировкой). Например, для 4-байтового вектора в нотации MSB [0x4, 0x3, 0x2, 0x1] вывод для элементов байта, слова и элементов слова будет следующим:

[0x4, 0x3, 0x2, 0x1] [0x0403, 0x0201] [0x04030201]

По сути, даже с байтового вывода вы можете просто "считывать" слово или выход dword или наоборот, так как байты уже находятся в обычном MSB-первом порядке для отображения номера. С другой стороны, соответствующий вывод для LSE-first:

[0x1, 0x2, 0x3, 0x4] [0x0201, 0x0403] [0x04030201]

Обратите внимание, что каждый слой подвергается свопам относительно строки над ним, поэтому гораздо труднее считывать большие или меньшие значения. Вам нужно будет больше полагаться на вывод элемента, наиболее естественного для вашей проблемы.

Этот формат также имеет то преимущество, что на архитектуре BE выход затем соответствует представлению в памяти одного и того же вектора, хранящегося в памяти 3.

Intel сначала использует MSE в своих руководствах.


1 Наименее значимый элемент

2 Такие нумерации предназначены не только для целей документации - они являются архитектурно видимыми, например, в масках в случайном порядке.

3 Конечно, это преимущество крохотное по сравнению с соответствующим преимуществом LSE-первых на платформах LE, так как BE почти мертв в аппаратных средствах SIMD.

Ответ 1

Мое эмпирическое правило: сопоставить эквивалентный макет в памяти, поэтому, если у вас есть 0x1 0x2 0x3 ... 0xf в памяти, и вы загружаете его в векторный регистр, тогда отображение содержимого векторного регистра должно также выглядеть как 0x1 0x2 0x3 ... 0xf.

Если вы используете расширения формата %v для printf, которые поддерживаются некоторыми компиляторами (например, Apple gcc и clang), тогда это поведение, которое вы получаете, и я считаю это полезным, поскольку вы почти можете забыть о капризы маленькой сущности, например

#include <stdio.h>
#include <stdint.h>
#include <xmmintrin.h>

int main(void)
{
    uint8_t a[16] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };

    __m128i v = _mm_loadu_si128((__m128i *)a);

    printf("v = %#vx\n", v);
    printf("v = %#vhx\n", v);
    printf("v = %#vlx\n", v);

    return 0;
}

С подходящим компилятором это дает:

v = 0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xa 0xb 0xc 0xd 0xe 0xf 0x10
v = 0x201 0x403 0x605 0x807 0xa09 0xc0b 0xe0d 0x100f
v = 0x4030201 0x8070605 0xc0b0a09 0x100f0e0d

Ответ 2

Быть последовательным - это самое важное; Если я работаю над существующим кодом, который уже имеет LSE-первые комментарии или имена переменных, я согласен с этим.

Учитывая выбор, я предпочитаю MSE-первую нотацию в комментариях, особенно при создании чего-то с тасованием или, особенно, упаковки/распаковки в разные размеры элементов.

Intel использует MSE-first не только в своих диаграммах в руководствах, но и в именах встроенных/инструкций типа pslldq (сдвиг байтов) и psrlw (бит-сдвиг): левый бит/байт сдвиг идет к MSB. Первое мышление LSE не спасает вас от умственно меняющихся вещей, это значит, что вы должны это делать, думая о смене вместо нагрузок/магазинов. Поскольку x86 является малоподвижным, вам иногда приходится об этом думать.


В MSE - сначала думая о векторах, просто помните, что порядок памяти справа налево. Когда вам нужно подумать о перекрытии невыложенных нагрузок из блока памяти, вы можете нарисовать содержимое памяти в порядке справа налево, чтобы вы могли смотреть на его векторные окна.

В текстовом редакторе нет проблем с добавлением нового текста в левой части чего-либо и смещения существующего текста вправо, поэтому добавление большего количества элементов в комментарий не является проблемой.

Два основных недостатка для MSE-первых нот:

  • сложнее ввести алфавит назад (например, h g f e | d c b a для вектора AVX из 32-битных элементов), поэтому я иногда просто начинаю с права и набираю a, стрелку влево, b, space, ctrl-left arrow, c, пробел,... или что-то в этом роде.

  • В отличие от порядка инициализации массива C. Обычно это не проблема, потому что _mm_set_epi* использует MSE-первый порядок. (Используйте _mm_setr_epi* для соответствия LSE-первым комментариям).


Примером, когда MSE-first является приятным, является попытка создания перекрестной версии 256b vpalignr: см. мой ответ на этот вопрос Как эффективно конкатенировать два вектора с помощью AVX2?. Это включает в себя ноты проектирования в первых нотах MSE.

В качестве другого примера рассмотрим возможность применения байта-сдвига переменной-значения по всему вектору. Вы могли бы составить таблицу векторов управления pshufb, но это будет огромная трата отпечатка кеша. Гораздо лучше загрузить скользящее окно из памяти:

/*  Example of using MSE notation for memory as well as vectors

// 4-element vectors to keep the design notes compact
// I started by just writing down a couple rows of this, then noticing which way they lined up
<< 3:                       00 FF FF FF
<< 1:                 02 01 00 FF
   0:              03 02 01 00
>> 2:        FF FF 03 02
>> 3:     FF FF FF 03
>> 4:  FF FF FF FF

       FF FF FF FF 03 02 01 00 FF FF FF FF
  highest address                       lowest address
*/

#include <immintrin.h>
#include <stdint.h>
// positive counts are right shifts, negative counts are left
// a left-only or right-only implementation would only have one side of the table,
// and only need 32B alignment for the constant in memory to prevent cache-line splits.
__m128i vshift(__m128i v, intptr_t bytes_right)
{   // intptr_t means the caller has to sign-extend it to the width of a pointer, saving a movsx in the non-inline version

   // C11 uses _Alignas, C++11 uses alignas
    _Alignas(64) static const int32_t shuffles[] = { 
        -1, -1, -1, -1,
        0x03020100, 0x07060504, 0x0b0a0908, 0x0f0e0d0c,
        -1, -1, -1, -1
    };  // compact but messy with a mix of ordering :/
    const char *identity_shuffle = 16 + (const char*)shuffles;  // points to the middle 16B

    //  count &= 0xf;  tricky to efficiently limit the count while still allowing >>16 to zero the vector, and to allow negative.
    __m128i control = _mm_load_si128((const __m128i*) (identity_shuffle + bytes_right));
    return _mm_shuffle_epi8(v, control);
}

Это наихудший вариант для MSE-first, потому что правые сдвиги берут окно слева. В LSE-первых нотациях это может выглядеть более естественно. Тем не менее, если я не получил что-то назад: P, я думаю, это показывает, что вы можете успешно использовать MSE-первую нотацию даже для того, что вы ожидаете быть сложным. Он не чувствовал сгибания ума или чрезмерного усложнения. Я только начал записывать векторы управления перемещением, а затем выстроил их. Я мог бы сделать это немного проще при переводе на массив C, если бы я использовал uint8_t shuffles[] = { 0xff, 0xff, ..., 0, 1, 2, ..., 0xff };. Я не тестировал это, только который он компилирует в одну команду:

    vpshufb xmm0, xmm0, xmmword ptr [rdi + vshift.shuffles+16]
    ret

MSE позволяет более легко заметить, когда вы можете использовать бит-смену вместо инструкции тасования, чтобы уменьшить давление на порт 5. Например, psllq xmm, 16/_mm_slli_epi64(v,16), чтобы сдвинуть слова, оставленные одним (с обнулением на границах qword). Или когда вам нужно сдвинуть элементы байта, но единственные доступные смены - 16 бит или шире. Самые узкие сдвиги с переменными на элемент - это 32-битные элементы (vpsllvd).

MSE упрощает получение постоянной тасования при использовании больших или меньших перетасовки зернистости или смесей, например. pshufd, когда вы можете удерживать пары элементов слова вместе, или pshufb, чтобы перетасовать слова по всему вектору (поскольку pshuflw/hw ограничен).

_MM_SHUFFLE(d,c,b,a) также входит в порядок MSE. Так же как и любой другой способ записать его как одно целое, например С++ 14 0b11'10'01'00 или 0xE4 (тождественное перетасовка). Использование LON-first notation заставит ваши константы тасования выглядеть "назад" относительно ваших комментариев. (за исключением констант pshufb, которые вы можете написать с помощью _mm_setr)