К сожалению, у меня не получилось так быстро, как хотелось бы, поэтому я оставлю вопрос открытым, если кто-то знает ответ получше.
Откуда возникла проблема.
Я искал, как реализовать инклюзивное сканирование на месте поверх AVX2. Расширения SIMD. Мое решение полностью основано на: @Zboson ответе.
[a b c d ]
+ [0 a b c ]
= [a (a + b) (b + c) (c + d) ]
+ [0 0 a (a + b) ]
= [a (a + b) (a + b + c) (a + b + c + d) ]
Каждый алгоритм диапазона, который я реализовал ранее, хорошо работал со следующим шаблоном итерации (код sudo):
auto aligned_f = previous_aligned_address(f);
auto aligned_l = previous_aligned_address(l);
ignore_first_n ignore_first{f - aligned_f};
if (aligned_f != aligned_l) {
step(aligned_f, ignore_first); // Do a simd step, ignoring everything
// between aligned_f and f.
aligned_f += register_width;
ignore_first = ignore_first_n{0};
// Big unrolled loop.
main_loop(aligned_f, aligned_l);
if (aligned_f == aligned_l) return;
}
ignore_last_n ignore_last {aligned_l + register_width - l};
ignore_first_last ignore = combine(ignore_first, ignore_last);
// Do a simd step, ignoring everything between aligned_l and l.
// + handle the case when register is bigger than the array size.
step(aligned_l, ignore);
(Если вы не знаете, почему это нормально - см.).
Как упоминалось в @PeterCordes и @PaulR, если вы измените шаблон итерации - смешайте некоторые другие значения и сделайте простое невыровненное хранилище, и это, вероятно, то, что мне придется сделать. Тогда вы можете сделать не более одного истинно замаскированного хранилища - только когда регистр не подходит полностью.
Однако это больше сгенерировано сборкой, и я не был уверен, реализовал ли я store(address, register, ignore)
наиболее эффективным способом - отсюда и был мой вопрос.
ОБНОВЛЕНИЕ: попробовал это, даже ничего не смешивая, вы можете просто сначала загрузить 2 перекрывающихся регистра, а затем сохранить их обратно. Все стало немного хуже. Это не кажется хорошей идеей, по крайней мере, для инклюзивного сканирования.
Измерения
Достаточно быстрый я определил как «превзойти скалярную версию на 40 байтах данных» - 40 символов, 20 кратких и 10 целых чисел. Вы могли заметить, что 40 байт больше размера регистра, поэтому мне пришлось бы добавить еще меньшее измерение для более сложного шаблона итераций.
Я показываю измерения для 2 случаев ‹256, 1> - использовать 256-битные регистры, без разворачивания,‹ 256, 2> - дважды развернуть основной цикл.
ПРИМЕЧАНИЕ. В тестах я учитываю возможные проблемы с выравниванием кода, выравнивая код тестирования 64 различными способами и выбирая минимальное значение.
_mm_maskmoveu_si128
Первоначально я выбрал _mm256_maskstore
для sizeof(T) >= 4
и 2 _mm_maskmoveu_si128
для остальных.
![_ mm_maskmoveu_si128 benchmarks](https://i.stack.imgur.com/DsVZc.png)
Это, как вы можете видеть, - выполнено очень плохо - для char
мы проигрываем скалярному коду примерно 10 раз, примерно 20 раз для short
и 2 раза для int
.
Используйте memcpy для char
и short
Я пробовал несколько разных вещей: использовать _mm256_maskstore
для short
, memcpy
для int
, написать собственный встроенный memcpy
для моего случая. Лучшее, что я получил: memcpy
за char
и short
и maskstore
за int
.
![memcpy / maskstore benchmark](https://i.stack.imgur.com/sYBZ4.png)
Это выигрыш для char
, разница в пару наносекунд между использованием без развертывания и двойным развертыванием, около 30% потерь для short
и 50% потерь для int
.
Итак, по крайней мере, с моей реализацией store(ptr, reg, ignore)
мне нужно сделать другой шаблон итераций, если я не хочу очищать циклы.
Объявление для store(addr, reg, ignore)
ПРИМЕЧАНИЕ: я удалил оболочки и адаптеры, возможно, добавил несколько ошибок.
// Only showing one ignore_broadcast, they are very similar and
// are actually generated with templates
template <register_256 Register, std::same<int> T>
inline __m256i ignore_broadcast(ignore_first_n ignore) {
__m256i idxs = _mm256_set_epi32(7, 6, 5, 4, 3, 2, 1, 0);
__m256i n_broadcasted = _mm256_set1_epi32(ignore.n - 1);
return _mm256_cmpgt_epi32(idxs, n_broadcasted);
}
template <template Register, typename T, typename Ignore>
void store(Register reg, T* ptr, Ignore ignore) {
if constexpr (sizeof(T) >= 4) {
const auto mask = ignore_broadcast<Register, T>(ignore);
_store::maskstore(ptr, mask, reg);
return;
}
std::size_t start = 0, n = sizeof(reg) / sizeof(T);
if constexpr (std::is_same_v<Ignore, ignore_first_n>) {
start += ignore.n;
n -= ignore.n;
} else if constexpr (std::is_same_v<Ignore, ignore_last_n>) {
n -= ignore.n;
} else {
static_assert(std::is_same_v<Ignore, ignore_first_last>);
start += ignore.first_n;
n -= ignore.first_n + ignore.last_n;
}
// This requires to store the register on the stack.
std::memcpy(raw_ptr + start, reinterpret_cast<T*>(®) + start, n * sizeof(T));
}
Что делает memcpy
Это memcpy вызывается.
Он реализует копирование менее 32 байт следующим образом:
#if VEC_SIZE > 16
/* From 16 to 31. No branch when size == 16. */
L(between_16_31):
vmovdqu (%rsi), %xmm0
vmovdqu -16(%rsi,%rdx), %xmm1
vmovdqu %xmm0, (%rdi)
vmovdqu %xmm1, -16(%rdi,%rdx)
ret
#endif
L(between_8_15):
/* From 8 to 15. No branch when size == 8. */
movq -8(%rsi,%rdx), %rcx
movq (%rsi), %rsi
movq %rcx, -8(%rdi,%rdx)
movq %rsi, (%rdi)
ret
L(between_4_7):
/* From 4 to 7. No branch when size == 4. */
movl -4(%rsi,%rdx), %ecx
movl (%rsi), %esi
movl %ecx, -4(%rdi,%rdx)
movl %esi, (%rdi)
ret
L(between_2_3):
/* From 2 to 3. No branch when size == 2. */
movzwl -2(%rsi,%rdx), %ecx
movzwl (%rsi), %esi
movw %cx, -2(%rdi,%rdx)
movw %si, (%rdi)
ret
Итак, в основном - возьмите самый большой регистр, который подходит, и сделайте два перекрывающихся магазина. Я попытался сделать это встроенным образом - вызов memcpy
был быстрее - хотя, возможно, я поступил неправильно.
Сборка и код
Чтение моего кода может быть немного сложным, особенно потому, что я полагаюсь на библиотеку eve
, которая еще не является открытым исходным кодом.
Итак, я собрал и опубликовал пару листингов сборок:
, a hrefling для отмены сборки = "https://github.com/DenisYaroshevskiy/unsq_eve/blob/7c01a4631c393330b11776b36082a390a011e81f/disassemble/disassemble.s#L54" rel = "nofollow noreferrer"> Короче полная сборка, без разворачивания
Мой код можно найти здесь
PS: Измерение большого размера
Если вам интересно, на достаточно большом массиве этот тип векторизации - хорошая победа. Например, на 10'000 байтах.
![измерение большого размера](https://i.stack.imgur.com/HpAp3.png)
Примерно 5 раз для символов, 3 раза для коротких и 2 раза для целых.
PS: при развертывании
Я не придумал какой-то умной раскрутки. Самая простая двойная развертка дает около 10% выигрыша для 10000 байт short
. Развертывание еще не помогло. Я подозреваю, что причина того, что выигрыш так мала, в том, что алгоритм довольно сложен.
![развертка измерений](https://i.stack.imgur.com/2cOlr.png)
person
Denis Yaroshevskiy
schedule
06.06.2020
vmovdqu8
. А пока вы можете проверить маску на наличие одинаковых парshort
элементов, поэтомуepi32
будет работать, в противном случае я думаю, вам придется перебирать вектор и делать узкие скалярные хранилища. Или что чтз сказал: вектор смешивается со старым содержимым памяти. Вероятно, это будет лучше, чем что-то проверять с битами маски. - person Peter Cordes   schedule 04.06.2020memcpy
- это лучше моего умного решения. Дело в том, что даже на 1К данных у меня полностью преобладают хранилища масок на стороне. Я пробовал базовый memcpy, он работает лучше, чем мои умные хаки. Хотя, наверное, есть хаки получше. - person Denis Yaroshevskiy   schedule 04.06.2020memcpy
? Не для маскировки произвольных элементов посередине? Обычно лучшая стратегия - выполнить векторную загрузку, которая заканчивается в конце исходного массива, и сохранить ее в соответствующем месте в месте назначения. Это нормально, что он может перекрывать последнее полное векторное хранилище; буфер хранилища / кеш L1d может поглотить это без проблем. ЦП с AVX также имеют эффективные невыровненные нагрузки / хранилища. - person Peter Cordes   schedule 04.06.2020memcpy
, по крайней мере, если вы работаете в такой системе, как GNU / Linux, где memcpy использует AVX в системах, которые его поддерживают. glibcmemcpy
очень хорошо оптимизирован для больших копий, включая обработку начала и конца копии. И да,_mm_maskmoveu_si128
имеет подсказку NT (удаляется из кеша), так что она вам определенно не нужна. - person Peter Cordes   schedule 04.06.2020inclusive_scan
, и мне нужно обрабатывать все возможные размеры массива, в частности, можно получить массив с размером меньше моего векторного. Я могу смешаться с предыдущим массивом, который я сохранил, но это означает больше особых случаев и больше раздуваемого кода. - person Denis Yaroshevskiy   schedule 04.06.2020memcpy
уже имеет это раздувание, чтобы быстро делать маленькие копии, а также большие копии, если предсказание ветвления предсказывает правильно. Я до сих пор не понимаю, может ли ваша настоящая проблема просто вызватьmemcpy
, или вам нужно избежать этого по какой-то причине. - person Peter Cordes   schedule 04.06.2020maskstore
для целых чисел, и все равно медленнее, чем мне бы хотелось. Думаю, у меня получится лучше. - person Denis Yaroshevskiy   schedule 04.06.2020_mm_maskstore_epi32
4-байтового выравнивания? - person Denis Yaroshevskiy   schedule 05.06.2020