В качестве эксперимента я пытался выяснить, как выполнить загрузку/сохранение маски с помощью ushorts
, превышающего фактическое требование.
Не существует вызова API для MaskLoad
/MaskStore
, который принимает указатель на ushort
, поэтому я подумал, что вам, возможно, придется просто сделать это с указателем int?, что я и попытался сделать ниже.
Что касается TestUShort
, я установил смещение от начала массива, чтобы пропустить первые два ushorts
.
ushort* address = ptr + 2;
Затем я создал маску,
[0xFFFF0000, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF]
который, как я думал, будет вести себя следующим образом (X — включить, 0 — замаскировать)
[X,0, X,X, 0,0, X,X, 0,0, X,X, X,X, X,X]
Поэтому я ожидаю
0, 0 <-- (first two skipped) ...12, 0, 12, 12, 0, 0, 12, 12, 0, 0, 12, 12, 12, 12, 12, 12
но вместо этого я получаю:
0, 0 <-- (first two skipped) ...12, 12, 12, 12, 0, 0, 12, 12, 0, 0, 12, 12, 12, 12, 12, 12
Это потому, что вы можете маскировать только блоки по 4 байта? Есть ли способ сделать это? Меня также интересует влияние такой операции на производительность на ushorts со смещением и маской, которая, как я полагаю, будет очень дорогостоящей из-за невыравнивания? Поскольку код в настоящее время не работает, я еще не тестировал его, поскольку никакие тайминги не имеют реального значения, если код не работает.
static unsafe void TestUShort()
{
ushort[] values = new ushort[256];
Vector256<uint> mask = Vector256.Create<uint>([0xFFFF0000, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF]);
Vector256<ushort> add = Vector256.Create<ushort>(12);
fixed (ushort* ptr = values)
{
ushort* address = ptr + 2;
uint* addressInt = (uint*)address;
var v = Avx2.MaskLoad(addressInt, mask).As<uint, ushort>();
v = Avx2.AddSaturate(v, add);
Avx2.MaskStore(addressInt, mask, v.As<ushort, uint>());
}
}
static unsafe void TestUInt()
{
uint[] uintValues = new uint[256];
Vector256<uint> mask = Vector256.Create<uint>([0xFFFFFFFF, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF]);
Vector256<uint> add = Vector256.Create<uint>(12);
fixed (uint* ptr = uintValues)
{
var v = Avx2.MaskLoad(ptr, mask);
v = Avx2.Add(v, add);
Avx2.MaskStore(ptr, mask, v);
}
}
Не существует инструкции AVX2, которая непосредственно выполняет замаскированное сохранение слов. Вы не можете использовать для этого хранилище в маске dword, потому что:
Бит маски для каждого элемента данных является старшим битом этого элемента в первом исходном операнде. Если маска равно 1, соответствующий элемент данных копируется из второго исходного операнда в целевой операнд. Если маска равна 0, соответствующий элемент данных устанавливается в ноль в форме загрузки этих инструкций и не изменяется в форма магазина.
MASKMOVDQU
(Sse2.MaskMove) может выполнять маскированное сохранение на побайтовой основе, однако он использует вневременную подсказку, и это может быть хорошо (предположительно) именно в том случае использования, для которого оно предназначено. (по крайней мере, в руководстве говорится, что это для чего-то полезно, на самом деле я никогда не использовал его успешно), в обычных случаях это в большинстве случаев плохо. Например, в Zen 3 это занимает 75 мкс и может выполняться только один раз каждые 18 тактов. Кроме того, это всего лишь 128-битная операция, поэтому вам понадобится две из них.
В большинстве случаев вы можете реализовать хранилище по маске, загрузив пункт назначения, смешав ваши новые данные со старыми данными в соответствии с маской и сохранив все это обратно. Есть две проблемы, которые могут помешать вам использовать эту технику:
В особом случае добавления 12 к некоторому подмножеству слов вы можете добавить 12 к элементам, которые вы хотите изменить, и 0 к элементам, которые вы не хотите менять, с учетом тех же двух проблем. Довольно часто можно сделать что-то подобное вместо полного смешивания со старыми данными, но, конечно, иногда вам просто нужно смешать.
Меня также интересует влияние такой операции на производительность на ushorts со смещением и маской, которая, как я полагаю, будет очень дорогостоящей из-за невыравнивания?
Все процессоры с поддержкой AVX2 достаточно хорошо справляются с (нормальной) невыровненной загрузкой и сохранением. Обычно это не имеет большого значения, как это было на процессорах эпохи Core2. Разделенные блокировки по-прежнему плохи, но вы выполняете операции SIMD, а не операции RMW с lock
.
В любом случае, я не подтвердил свое предположение, но думаю, что дизайн maskmovdqu
, возможно, подходил старым процессорам, но не новым процессорам. Он по-прежнему поддерживается для совместимости, но, возможно, это не лучший выбор для производительности на современных процессорах.
Я попробовал использовать BlendVariable, и это решило мою проблему, однако теперь у меня возникла еще одна проблема, о которой я не подумал. В моем сценарии маска смешивания должна быть динамической. то есть мне нужно что-то вроде var blendMask = Vector256<ushort>.Create(numOfElementsToInclude)
... есть ли что-нибудь подобное, о чем вы могли бы знать? или, возможно, возможность сдвинуться и оставить всю маску? таким образом я мог бы начать со всех FFFF и т. д., а затем сдвинуть их на количество мест, чтобы создать динамическую маску?
@creativergk вы можете превратить несколько элементов в соответствующую маску, выполнив невыровненную загрузку, используя 2*(16 - numberOfElements)
в качестве смещения в память, в которой хранится 0xffff, 0xffff, ...., 0, 0
ой! умный. Буду ли я использовать динамическую память или есть способ сделать это в стеке для повышения производительности?
@creativergk, что бы вы ни захотели, вы можете сделать это в стеке, например, с помощью массива stackalloc
, но я думаю, вы можете сделать это, не выделяя какой-либо массив. Я забыл подробности, но IIRC, получающий диапазон только для чтения для константы массива, по сути, компилируется в получение указателя на массив в сегменте данных, без какого-либо выделения.
@creativergk может понравиться это, я не знаю, но это показывает концепцию. Никакого распределения.
Да, я именно так и пробовал, работает отлично. Я проведу несколько тестов, чтобы посмотреть, как это повлияет на производительность.
кажется, работает немного лучше с public static readonly ushort[] = ...
@creativergk, вы также можете попробовать передать счетчик и сравнить с ним вектор 0, 1, 2, 3, 4 ...
, в месте, чувствительном к задержке, это, вероятно, лучше
Я не уверен, что
maskmovdqu
когда-либо будет хорошо работать на каких-либо многоядерных процессорах. Он (или версия MMX) мог бы быть хорош на старых одноядерных процессорах, у которых было меньше проблем с когерентностью кэша, и, возможно, мог бы напрямую транслировать замаскированное хранилище в команду контроллера DRAM, которая использует побайтовые строки включения для Burst, которые раньше были обязательными, но являются необязательными, по крайней мере, в DDR4. (Однако одноядерные процессоры не имели встроенных контроллеров памяти, за исключением некоторых ранних моделей AMD, поэтому хранилище все равно должно было идти через переднюю шину к северному мосту.)