Расширяющий регистр с узким типом T
= uint8_t
или uint16_t
, вероятно, лучше всего реализовать с SSSE3 pmaddubsw
или SSE2 pmaddwd
с множителем 1
. ( Руководство по встроенным функциям ) Эти инструкции используются по одному и точно расширяют горизонтальное пространство, добавляя, что вам нужно более эффективно, чем перетасовка.
Если вы можете сделать это без потери точности, сделайте вертикальное сложение между строками сначала , прежде чем расширять горизонтальное, добавьте . (например, 10, 12 или 14-битные пиксельные компоненты в [u]int16_t
не могут переполниться). Загрузка и вертикальное добавление имеют (по крайней мере) 2 на тактовую частоту на большинстве процессоров, по сравнению с 1 на такт для pmadd*
, имеющей только 2 тактовых такта на Skylake и более поздних версиях. И это означает, что вам нужно только 1x add + 1x pmadd против 2x pmadd + 1x add, так что это значительный выигрыш даже на Skylake. (Для 2-го способа обе загрузки могут сложиться в операнды памяти для pmadd, если у вас есть AVX. Для способа добавления перед pmadd сначала вам понадобится чистая загрузка, а затем сложите вторую загрузку в add, чтобы вы не могли сохранить входные мопы, если не используете режимы индексированной адресации и они не ламинируются. ) * * тысяча двадцать-один
И в идеале вам не нужно +=
в массив аккумуляторов, и вместо этого вы можете просто читать 2 строки параллельно, и аккумулятор предназначен только для записи, поэтому ваш цикл имеет только 2 входных потока и 1 выходной поток.
// SSSE3
__m128i hadd_widen8_to_16(__m128i a) {
// uint8_t, int8_t (doesn't matter when multiplier is +1)
return _mm_maddubs_epi16(a, _mm_set_epi8(1));
}
// SSE2
__m128i hadd_widen16_to_32(__m128i a) {
// int16_t, int16_t
return _mm_madd_epi16(a, _mm_set_epi16(1));
}
Эти порты напрямую подключаются к 256-битному AVX2, поскольку ширина входа и выхода одинакова. Перестановка не требуется для исправления набивки на линии.
Да, действительно, они оба _epi16
. Intel может быть дико несовместимым с собственными именами. мнемоника asm более последовательна и легче запомнить что к чему. (ubsw
= байт без знака для подписанного слова, за исключением того, что один из входных данных является байтом со знаком. pmaddwd
упакован, умножить, добавить слово к dword, такая же схема именования, как у punpcklwd
и т. Д.)
Случай T = U с uint16_t
или uint32_t
является вариантом использования для SSSE3 _mm_hadd_epi16
или _mm_hadd_epi32
. Это стоит столько же, сколько 2 шаффла + вертикальное добавление, но в любом случае вам нужно упаковать 2 входа в 1.
Если вы хотите обойти узкое место в случайном порте в Haswell и более поздних версиях, вы можете рассмотреть возможность использования сдвигов qword на входах и затем перемешать результат с shufps
(_mm_shuffle_ps
+ некоторое приведение). Возможно, это может быть победой на Skylake (с 2-мя пропускными способностями на смену тактового генератора), даже несмотря на то, что он будет стоить 5 полных мопов вместо 3-х. Он может работать в лучшем случае 5/3 цикла на вектор выхода вместо 2 циклов на вектор, если есть нет узкого места переднего конца
// UNTESTED
//Only any good with AVX, otherwise the extra movdqa instructions kill this
//Only worth considering for Skylake, not Haswell (1/c shifts) or Sandybridge (2/c shuffle)
__m128i hadd32_emulated(__m128i a, __m128i b) {
__m128i a_shift = _mm_srli_epi64(a, 32);
__m128i b_shift = _mm_srli_epi64(b, 32);
a = _mm_add_epi32(a, a_shift);
b = _mm_add_epi32(b, b_shift);
__m128 combined = _mm_shuffle_ps(_mm_castsi128_ps(a), _mm_castsi128_ps(b), _MM_SHUFFLE(2,0,2,0));
return _mm_castps_si128(combined);
}
Для версии AVX2 вам понадобится перестановка переулка, чтобы исправить результат vphadd
. Так что эмуляция хэда со сменами может быть более выигрышной.
// 3x shuffle 1x add uops
__m256i hadd32_avx2(__m256i a, __m256i b) {
__m256i hadd = _mm256_hadd_epi32(a, b); // 2x in-lane hadd
return _mm256_permutex_epi64( hadd, _MM_SHUFFLE(3,1,2,0) );
}
// UNTESTED
// 2x shift, 2x add, 1x blend-immediate (any ALU port), 1x shuffle
__m256i hadd32_emulated_avx2(__m256i a, __m256i b)
{
__m256i a_shift = _mm256_srli_epi64(a, 32); // useful result in the low half of each qword
__m256i b_shift = _mm256_slli_epi64(b, 32); // ... high half of each qword
a = _mm256_add_epi32(a, a_shift);
b = _mm256_add_epi32(b, b_shift);
__m256i blended = _mm256_blend_epi32(a,b, 0b10101010); // alternating low/high results
return _mm256_permutexvar_epi32(_mm256_set_epi32(7,5,3,1, 6,4,2,0), blended);
}
На Haswell и Skylake hadd32_emulated_avx2
может работать с частотой 1 на 2 такта (насыщая все векторные порты ALU). Дополнительные add_epi32
для суммирования в accum[]
замедляют его до лучших 7/3 циклов на 256-битный вектор результатов, и вам нужно будет развернуть (или использовать компилятор, который развертывает), чтобы не просто узкое место на внешний интерфейс.
hadd32_avx2
может работать с частотой 1 на 3 такта (узкое место на порту 5 для перемешивания). Load + store + extra add_epi32
мопов для реализации вашего цикла может легко работать в тени.
(https://agner.org/optimize/, и см. https://stackoverflow.com/tags/x86/info)