Ответ 1
a
и b
разные для каждого пикселя? Это будет затруднять векторизация, если нет шаблона или вы можете сгенерировать их
Есть ли способ эффективно генерировать a
и b
в векторах как в виде фиксированной или плавающей запятой? Если нет, вставка 4 значений FP или 8 16-битных целых чисел может быть хуже, чем просто скалярные операции.
Фиксированная точка
Если a
и b
могут быть повторно использованы вообще или сгенерированы с фиксированной точкой в первую очередь, это может быть хорошим прецедентом для математики с фиксированной точкой. (т.е. целые числа, которые представляют собой шкалу значений * 2 ^). SSE/AVX не имеют умножения на 8b * 8b- > 16b; наименьшие элементы - слова, поэтому вам приходится распаковывать байты в слова, но не до 32-битного. Это означает, что вы можете обрабатывать в два раза больше данных для каждой команды.
Здесь есть инструкция _mm_maddubs_epi16
, которая может быть полезна, если b
и a
изменяются нечасто, или вы можете легко сгенерировать вектор с чередующимися байтами * 2 ^ 4 и b * 2 ^ 1. По-видимому, это действительно удобно для билинейной интерполяции, но он по-прежнему выполняет задание для нас с минимальной перетасовкой, если мы можем подготовить a и b вектор.
float a, b;
const int logascale = 4, logbscale=1;
const int ascale = 1<<logascale; // fixed point scale for a: 2^4
const int bscale = 1<<logbscale; // fixed point scale for b: 2^1
const __m128i brescale = _mm_set1_epi8(1<<(logascale-logbscale)); // re-scale b to match a in the 16bit temporary result
for (i=0 ; i<n; i+=16) {
//__m128i avec = get_scaled_a(i);
//__m128i bvec = get_scaled_b(i);
//__m128i ab_lo = _mm_unpacklo_epi8(avec, bvec);
//__m128i ab_hi = _mm_unpackhi_epi8(avec, bvec);
__m128i abvec = _mm_set1_epi16( ((int8_t)(bscale*b) << 8) | (int8_t)(ascale*a) ); // integer promotion rules might do sign-extension in the wrong place here, so check this if you actually write it this way.
__m128i block = _mm_load_si128(&buf[i]); // call this { v[0] .. v[15] }
__m128i lo = _mm_unpacklo_epi8(block, brescale); // {v[0], 8, v[1], 8, ...}
__m128i hi = _mm_unpackhi_epi8(block, brescale); // {v[8], 8, v[9], 8, ...
lo = _mm_maddubs_epi16(lo, abvec); // first arg is unsigned bytes, 2nd arg is signed bytes
hi = _mm_maddubs_epi16(hi, abvec);
// lo = { v[0]*(2^4*a) + 8*(2^1*b), ... }
lo = _mm_srli_epi16(lo, logascale); // truncate from scaled fixed-point to integer
hi = _mm_srli_epi16(hi, logascale);
// and re-pack. Logical, not arithmetic right shift means sign bits can't be set
block = _mm_packuswb(lo, hi);
_mm_store_si128(&buf[i], block);
}
// then a scalar cleanup loop
2 ^ 4 - произвольный выбор. Он оставляет 3 бита без знака для целой части a
и 4 бит бит. Таким образом, он эффективно округляет a
до ближайшего 16-го и переполняет, если он имеет величину больше 8 и 15/16-й. 2 ^ 6 даст больше дробных битов и позволит a
от -2 до +1 и 63/64ths.
Поскольку b
добавляется, а не умножается, его полезный диапазон намного больше, а дробная часть гораздо менее полезна. Чтобы представить его в 8 бит, округление его до ближайшей половины по-прежнему содержит немного дробной информации, но позволяет ему быть [-64: 63.5] без переполнения.
Для большей точности 16-битная фиксированная точка является хорошим выбором. Вы можете масштабировать a
и b
вверх на 2 ^ 7 или что-то еще, чтобы иметь 7b дробной точности и все же позволить целочисленной части быть [-256.. 255]. Нет инструкции умножения и добавления для этого случая, поэтому вам придется делать это отдельно. Хорошие варианты для умножения включают:
-
_mm_mulhi_epu16
: unsigned 16b * 16b- > high16 (бит [31:16]). Полезно, еслиa
не может быть отрицательным -
_mm_mulhi_epi16
: подписан 16b * 16b- > high16 (бит [31:16]). -
_mm_mulhrs_epi16
: подписанный 16b * 16b- > бит [30:15] из 32b временный, с округлением. При хорошем выборе коэффициента масштабирования дляa
это должно быть лучше. Насколько я понимаю, SSSE3 представил эту инструкцию для такого использования. -
_mm_mullo_epi16
: подписан 16b * 16b- > low16 (бит [15: 0]). Это позволяет только 8 значащим битам дляa
до переполнения результата low16, поэтому я думаю, что все, что вы получаете по сравнению с_mm_maddubs_epi16
8-битным решением, является большей точностью дляb
.
Чтобы использовать их, вы получите масштабированные 16b-векторы значений a
и b
, а затем:
- распакуйте байты с нулем (или
pmovzx
byte- > word), чтобы получить подписанные слова еще в диапазоне [0..255] - сдвиньте слова слева на 7.
- умножьте ваш вектор
a
на 16b слов, взяв верхнюю половину каждого результата 16 * 16- > 32. (например, mul - сдвиг вправо здесь, если вы хотите разные шкалы для
a
иb
, чтобы получить более дробную точность дляa
- добавьте
b
к этому. - сдвиг вправо, чтобы окончательная усечка вернулась с неподвижной точки на [0..255].
При хорошем выборе шкалы с фиксированной точкой это должно иметь возможность обрабатывать более широкий диапазон a
и b
, а также более дробную точность, чем 8-битная фиксированная точка.
Если вы не сдвигаете свои байты после распаковки их слов, a
должен быть полным, чтобы получить 8 бит в высоком 16 результата. Это означало бы очень ограниченный диапазон a
, который вы могли бы поддерживать, не сокращая временное до менее 8 бит во время умножения. Даже _mm_mulhrs_epi16
не оставляет много места, так как он начинается с бит 30.
расширять байты до плавающих
Если вы не можете эффективно генерировать значения фиксированной точки a
и b
для каждого пикселя, лучше всего конвертировать пиксели в плавающие. Это требует больше распаковки/переупаковки, поэтому задержки и пропускная способность хуже. Стоит посмотреть на создание a и b с фиксированной точкой.
Для работы с плавающей запятой вам по-прежнему необходимо эффективно создавать вектор значений a
для 4 смежных пикселей.
Это хороший прецедент для pmovzx
(SSE4.1), потому что он может перейти непосредственно из элементов 8b в 32b. Другими параметрами являются SSE2 punpck[l/h]bw/punpck[l/h]wd
с несколькими шагами или SSSE3 pshufb
для эмуляции pmovzx
. (Вы можете сделать одну нагрузку 16B и перетасовать ее 4 разных способа распаковать ее до четырех векторов 32b ints.)
char *buf;
// const __m128i zero = _mm_setzero_si128();
for (i=0 ; i<n; i+=16) {
__m128 a = get_a(i);
__m128 b = get_b(i);
// IDK why there isn't an intrinsic for using `pmovzx` as a load, because it takes a m32 or m64 operand, not m128. (unlike punpck*)
__m128i unsigned_dwords = _mm_cvtepu8_epi32((__m128i)(buf+i)); // load 4B at once.
__m128 floats = _mm_cvtepi32_ps(unsigned_dwords);
floats = _mm_fmadd_ps(floats, a, b); // with FMA available, this might as well be 256b vectors, even with the inconvenience of the different lane-crossing semantics of pmovzx vs. punpck
// or without FMA, do this with _mm_mul_ps and _mm_add_ps
unsigned_dwords = _mm_cvtps_epi32(floats);
// repeat 3 more times for buf+4, buf+8, and buf+12, then:
__m128i packed01 = _mm_packss_epi32(dwords0, dwords1); // SSE2
__m128i packed23 = _mm_packss_epi32(dwords2, dwords3);
// packuswb wants SIGNED input, so do signed saturation on the first step
// saturate into [0..255] range
__m12i8 packedbytes=_mm_packus_epi16(packed01, packed23); // SSE2
_mm_store_si128(buf+i, packedbytes); // or storeu if buf isn't aligned.
}
// cleanup code to handle the odd up-to-15 leftover bytes, if n%16 != 0
Предыдущая версия этого ответа переходила от векторов float- > uint8 с пакетом packusdw/packuswb и имела полный раздел об обходных решениях без SSE4.1. Ни один из этих символов маскировки-знака после неподписанного пакета не требуется, если вы просто остаетесь в объявленном целочисленном домене до последнего пакета. Я предполагаю, что именно по этой причине SSE2 включил только подписанный пакет от слова dword к слову, но как подписанный, так и неподписанный пакет от слова к байту. packuswd
полезен, если ваша конечная цель uint16_t
, а не дальнейшая упаковка.
Последним процессором, не имеющим SSE4.1, был Intel Conroe/merom (первый ген Core2, до конца 2007 года) и AMD pre Barcelona (до конца 2007 года). Если для этих процессоров приемлемо рабочее, но медленное, просто напишите версию для AVX2 и версию для SSE4.1. Или SSSE3 (с 4x pshufb для эмуляции pmovzxbd из четырех 32b элементов регистра) pshufb медленно работает на Conroe, поэтому, если вы заботитесь о CPU без SSE4.1, напишите конкретную версию. На самом деле Conroe/merom также имеет медленный xmm punpcklbw
и т.д. (За исключением q- > dq). 4x slow pshufb
должен по-прежнему проигрывать 6x медленных распаковки. Векторизация намного меньше выигрыша в пред-Wolfdale из-за медленных перетасовки для распаковки и переупаковки. Версия с фиксированной точкой, с гораздо меньшим количеством распаковки/переупаковки, будет иметь еще большее преимущество.
См. историю изменений для незавершенной попытки использования punpck
, прежде чем я понял, сколько дополнительных инструкций вам понадобится. Удалено, потому что этот ответ уже давно, а другой блок кода будет путать.