Конвенция для отображения векторных регистров
Существует ли соглашение для отображения / записи больших регистров, подобных тем, которые доступны в наборе команд 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]
обсуждение
Я думаю, что два наиболее многообещающих ответа - это просто "LSE1 first" (т.е. первый вывод в приведенных выше примерах) или "MSE first" (второй вывод). Ни то, ни другое не зависит от порядкового номера платформы, так как в действительности данные в регистре, как правило, не зависят от порядкового номера (как операции с регистром GP или long
или же int
или что-либо в C не зависит от порядка байтов). Endianness появляется в интерфейсе памяти регистра <->, и здесь я спрашиваю о данных, уже находящихся в регистре.
Возможно, что существуют другие ответы, такие как результат, который зависит от порядка байтов (и ответ Пола Р. может быть один, но я не могу сказать).
LSE First
Одним из преимуществ LSE-first, по-видимому, является особенно побайтный вывод: часто байты нумеруются от 0 до N, а LSB равен нулю2, поэтому выход LSB-first выводит его с возрастающими индексами, так же, как вы выводите массив байтов размера N.
Это также хорошо на архитектурах с прямым порядком байтов, поскольку выходные данные затем соответствуют представлению в памяти того же вектора, сохраненного в памяти.
MSE First
Основным преимуществом здесь, по-видимому, является то, что выходные данные для меньших элементов находятся в том же порядке, что и для больших размеров (только с другой группировкой). Например, для 4-байтового вектора в формате MSB [0x4, 0x3, 0x2, 0x1]
вывод для байтовых элементов, элементов word и dword будет:
[0x4, 0x3, 0x2, 0x1] [0x0403, 0x0201] [0x04030201]
По сути, даже из байтового вывода вы можете просто "прочитать" слово или слово, или наоборот, так как байты уже находятся в обычном MSB-первом порядке для отображения чисел. С другой стороны, соответствующий вывод для LSE-first:
[0x1, 0x2, 0x3, 0x4] [0x0201, 0x0403] [0x04030201]
Обратите внимание, что каждый слой подвергается перестановкам относительно строки над ним, поэтому намного сложнее считывать большие или меньшие значения. Вам нужно больше полагаться на вывод элемента, наиболее естественного для вашей проблемы.
Этот формат также имеет то преимущество, что на архитектурах BE выходные данные затем соответствуют представлению в памяти того же вектора, сохраненного в памяти3.
Intel использует MSE первым в своих руководствах.
1 наименее значимый элемент
2 Такие нумерации предназначены не только для целей документирования - они видны с точки зрения архитектуры, например, в случайных масках.
3 Конечно, это преимущество незначительно по сравнению с соответствующим преимуществом LSE-first на платформах LE, поскольку BE практически не используется в стандартном аппаратном обеспечении SIMD.
2 ответа
Быть последовательным - самая важная вещь; Если я работаю над существующим кодом, в котором уже есть комментарии или имена переменных, написанные на LSE, я подхожу к этому.
Учитывая выбор, я предпочитаю MSE-первую нотацию в комментариях, особенно когда создаю что-то с шаффлом или особенно упаковываем / распаковываем для элементов разных размеров.
Intel использует MSE-first не только в своих схемах в руководствах, но и в именах встроенных функций / инструкций, таких как pslldq
(сдвиг байтов) и psrlw
(сдвиг битов): сдвиг влево / бит идет в сторону MSB. Мышление LSE в первую очередь не спасает вас от мысленных изменений, это означает, что вы должны делать это, думая о сменах, а не о нагрузках / магазинах. Поскольку x86 имеет младший порядок, вам иногда все равно приходится думать об этом.
В MSE, сначала думая о векторах, просто помните, что порядок памяти справа налево. Когда вам нужно подумать о перекрывающихся невыровненных нагрузках из блока памяти, вы можете нарисовать содержимое памяти в порядке справа налево, чтобы вы могли посмотреть на его окна векторной длины.
В текстовом редакторе нет проблем с добавлением нового текста с левой стороны чего-либо и смещением существующего текста вправо, поэтому добавление дополнительных элементов в комментарий не является проблемой.
Два основных недостатка нотации MSE-first:
сложнее набрать алфавит в обратном направлении (как
h g f e | d c b a
для вектора AVX 32-битных элементов), поэтому я иногда просто начинаю справа и набираюa
, стрелка влево,b
, пробел, ctrl-стрелка влево,c
, космос,... или что-то в этом роде.В противоположность порядку инициализатора массива C. Обычно это не проблема, потому что
_mm_set_epi*
использует MSE-первый заказ. (Использование_mm_setr_epi*
чтобы соответствовать LSE-первые комментарии).
Пример, в котором MSE-first хорош, - это попытка разработать версию 256b с пересечением полос движения vpalignr
: Смотрите мой ответ на этот вопрос Как эффективно объединить два вектора с помощью AVX2?, Это включает примечания проекта в примечании MSE-first.
В качестве другого примера рассмотрим реализацию байтового сдвига с переменным числом по всему вектору. Вы могли бы сделать стол из 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, тоже. Так же как и любой другой способ записать его как одно целое число, например C++14 0b11'10'01'00
или же 0xE4
(тасование личности). Использование LSE-первой нотации заставит ваши случайные константы выглядеть "задом наперед" относительно ваших комментариев. (кроме pshufb
константы, которые вы можете написать с _mm_setr
)
Мое эмпирическое правило таково: сопоставьте эквивалентный макет в памяти, так что если у вас есть 0x1 0x2 0x3 ... 0xf
в памяти, и вы загружаете его в векторный регистр, то отображение содержимого векторного регистра также должно выглядеть 0x1 0x2 0x3 ... 0xf
,
Если вы используете %v
расширения формата для printf
которые поддерживаются некоторыми компиляторами (например, gcc и clang от Apple), тогда это поведение, которое вы получаете, и я считаю его полезным, так как вы можете почти забыть о капризах небольшой последовательности, например
#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