Сдвиг 4 целых чисел вправо на разные значения SIMD
SSE не предоставляет способа сдвига упакованных целых чисел на переменную величину (я могу использовать любые инструкции AVX и старше). Вы можете делать только однообразные смены. Результат, которого я пытаюсь достичь для каждого целого числа в векторе, таков.
i[0] = i[0] & 0b111111;
i[1] = (i[1]>>6) & 0b111111;
i[2] = (i[2]>>12) & 0b111111;
i[3] = (i[3]>>18) & 0b111111;
По существу пытается изолировать другую группу из 6 битов в каждом целом числе.
Так каково же оптимальное решение?
Вещи, о которых я думал:
Вы можете имитировать переменный сдвиг вправо, с переменным сдвигом влево и равномерным сдвигом вправо сдвиг. Я подумал о том, чтобы умножить упакованные целые числа на различную величину каждое (таким образом, имитируя сдвиг влево). Тогда с полученным результатом вы можете сделать равномерный сдвиг вправо и получить ответ. Проблема с этой конкретной операцией, которую я бы использовал для умножения, была бы _mm_mullo_epi32, которая имеет разочаровывающую задержку (10 циклов для haswell), и, учитывая мою программу, ей пришлось бы ждать результата, потому что этот конкретный результат является зависимостью для следующих инструкций. В целом этот метод я думаю было бы только немного быстрее, чем метод грубой силы, который распаковывает, сдвигает с помощью скалярных инструкций, а затем переупаковывает вектор, что займет около 20 циклов, я думаю.
1 ответ:
Если AVX2 доступен , для этого требуется только одна эффективная инструкция. например:
__m128i _mm_srlv_epi32 (__m128i a, __m128i count)(vpsrlvd), и 256-битные версии. Переменные сдвиги 32-битных и 64-битных элементов по соответствующим элементам счета доступны в левой, правой арифметике и правой логике. (Арифметический сдвиг вправо недоступен при 64-битном размере элемента.)AVX512BW добавляет 16-битные переменные сдвиги .
Эмулируя его без AVX2:
Что это за цепочка зависимостей? Можете ли вы развернуть и перемежать так, чтобы два вектора находились в полете одновременно? Две длинные цепочки dep параллельно гораздо лучше, чем одна длинная цепочка dep, если она настолько длинна, что окно out-of-order не может видеть следующую цепочку dep в следующей итерации цикла.
Возможно, стоит сделать отдельную версию AVX2 вашей функции для использования на процессорах Haswell и более поздних процессорах (где вы можете использоватьпеременную shift ). Если вы это сделаете, ваша функция будет использовать только
pmulld(mullo_epi32) на процессорах, где это наиболее эффективно. (то есть вы избегаете SSE4.1mullo_epi32на процессорах AVX2, потому что оказывается, что эти процессоры сделали эту инструкцию медленнее.)
pmulldпохоже, лучшее, что мы можем сделать для пропускной способности и плавленого домена uop рассчитывать даже на Haswell.На SnB / IvB, где это один uop для векторно-целочисленной единицы умножения, вся функция составляет только 2 uops / 6 Cycle latency / one на пропускную способность 1c. (Что еще хуже, чем мне удалось с shift / blend, так что вы хотели бы использовать только
pmulld, если пропускная способность / размер кода имеет значение вообще,и вы не узкое место чисто по задержке. например, после разворачивания.)Если ваши числа сдвигов являются постоянными, и у вас есть запасные биты в верхней части вашего регистра, вы можете умножить на степени 2, а затем использовать фиксированный сдвиг вправо. сдвиньте вправо каждый DW в __m128i на другую величину . Выбивание высоких битов не является проблемой для вашего извлечения битового поля, потому что вы собираетесь в любом случае держите только самые низкие биты.
// See the godbolt link below for a version of this with more comments // SnB/IvB: 6c latency, 2 fused-domain uops. __m128i isolate_successive_6bits_mul (__m128i input) { // We can avoid the AND if we shift the elements all the way to the left to knock off the high garbage bits. // 32 - 6 - 18 = 8 extra bits to left shift __m128i mul_constant = _mm_set_epi32(1<<(0+8), 1<<(6+8), 1<<(12+8), 1<<(18+8)); __m128i left_vshift = _mm_mullo_epi32(input, mul_constant); __m128i rightshifted = _mm_srli_epi32(left_vshift, (18+8)); return rightshifted; }
Умный способ с блендами:
(К сожалению, у нас нет AVX2
vpblenddдля эффективных смесей dword, которые могут работать на любом порту.pblendwограничивается портом 5 на процессорах Intel.blendpsможет быть хорошим для пропускной способности (работает на любом порту), но может вводить задержки обхода между целочисленными инструкциями.)Сдвигайте и смешивайте так, чтобы каждый элемент заканчивался правильным общим количеством сдвигов.
И-маскировать низкий уровень 6 бит после объединения всего в один вектор.
Та же задержка, что и у метода грубой силы (см. ниже) на процессорах Intel, и лучшая пропускная способность (из-за меньшего количества UOP). Только две немедленные смеси, связывающие port5, хороши. (AVX2
vpblenddможет работать на любом порту, но тогда мы просто используемvpsrlvd.)// seems to be optimal for Intel CPUs. __m128i isolate_successive_6bits (__m128i input) { // input = [ D C B A ] // output = [ D>>18 C>>12 B>>6 A ] & set1(0b111111) __m128i right12 = _mm_srli_epi32(input, 12); __m128i merged = _mm_blend_epi16(input, right12, 0xF0); // copy upper half, like `movhps` (but don't use that because of extra bypass delay) // merged = [ D>>12 C>>12 B>>0 A>>0 ] __m128i right6 = _mm_srli_epi32(merged, 6); merged = _mm_blend_epi16(merged, right6, 0b11001100); // blend in the odd elements // merged = [ D>>(12+6) C>>12 B>>(0+6) A>>0 ] return _mm_and_si128(merged, _mm_set1_epi32(0b111111)); // keep only the low 6 bits }Я поместил обе версии в проводник компилятора Godbolt.
Эта версия содержит только 5 uops, скомпилированных с gcc 5.3
-O3 -march=ivybridge:# input in xmm0, result in xmm0 isolate_successive_6bits: vpsrld xmm1, xmm0, 12 # starts on cycle 0, result ready for the start of cycle 1 vpblendw xmm0, xmm0, xmm1, 240 # cycle 1 vpsrld xmm1, xmm0, 6 # cycle 2 vpblendw xmm0, xmm0, xmm1, 204 # cycle 3 vpand xmm0, xmm0, XMMWORD PTR .LC0[rip] # cycle 4, result ready on cycle 5 retКаждая инструкция является зависит от предыдущего, поэтому имеет задержку 5С. Процессоры SnB/IvB/HSW/BDW имеют только один порт сдвига, поэтому они не могут воспользоваться преимуществами параллелизма, доступного в более грубой версии (которая делает три смены с различным количеством сдвигов). Скайлэйк может, но затем два цикла смешивания впоследствии съедают улучшение.
Способ "грубой силы" :
Сделайте три сдвига три разных счета сдвигов и используйте три немедленных смешивания (
pblendw) для объединения четыре вектора в один, который имеет каждый желаемый элемент.// same latency as the previous version on Skylake // slower on previous Intel SnB-family CPUs. isolate_successive_6bits_parallel: vpsrld xmm1, xmm0, 6 # cycle 0. SKL: c0 vpsrld xmm2, xmm0, 12 # cycle 1 (resource conflict on pre-Skylake). SKL: c0 vpblendw xmm1, xmm0, xmm1, 12 # cycle 2 (input dep). SKL: c1 vpsrld xmm3, xmm0, 18 # cycle 2. SKL: c1 vpblendw xmm0, xmm2, xmm3, 192 # cycle 3 (input dep). SKL: c2 vpblendw xmm0, xmm1, xmm0, 240 # cycle 4 (input dep). SKL: c3 vpand xmm0, xmm0, XMMWORD PTR .LC0[rip] # cycle 5 (input dep). SKL: c4. retВыполнение слияния с цепочкой линейных зависимостей вместо дерева означает, что слияние может завершиться раньше, чем будет готов результат последнего сдвига:
isolate_successive_6bits_parallel2: vpsrld xmm1, xmm0, 6 # c0. SKL:c0 vpsrld xmm2, xmm0, 12 # c1. SKL:c0 vpblendw xmm1, xmm0, xmm1, 12 # c2. SKL:c1 vpblendw xmm1, xmm1, xmm2, 48 # c3. SKL:c2 vpsrld xmm0, xmm0, 18 # c2. SKL:c1 vpblendw xmm0, xmm1, xmm0, 192 # c4. SKL:c3 (dep on xmm1) vpand xmm0, xmm0, XMMWORD PTR .LC0[rip] # c5. SKL:c4 retХм, нет, не помогает. Никакого выигрыша в задержку для ШНБ регистрироваться, или для СКЛ. Первое слияние может произойти только после одного сдвига, потому что несмещенный вход-это то, что нам нужно для одного элемента. Если бы элемент 0 нуждался в ненулевом количестве сдвигов, этот способ имел бы преимущество для пре-СКЛ, и, возможно, недостаток для СКЛ.
Comments