Я немного поиграл с этим.Я думаю, что лучшее решение состоит в том, чтобы разделить входные данные из двух регистров на каналы из 16-битных целых чисел (то есть, 8-битное целое число с чередованием 0x00
).Затем выполните фактическое масштабирование (используя только 6 умножений + 3 сдвига для 8 пикселей вместо 8 + 4 в исходном подходе), а затем снова объедините каналы в пиксели.
Подтверждение концепции (не проверено)при условии, что ввод выровнен и число пикселей кратно 8, версия 2.0 (см. history для предыдущей версии):
void alpha_premultiply(__m128i *input, int length)
{
for(__m128i* last = input + (length & ~1); input!=last; input+=2)
{
// load data and split channels:
__m128i abgr = _mm_load_si128(input);
__m128i ABGR = _mm_load_si128(input+1);
__m128i __ab = _mm_srli_epi32(abgr,16);
__m128i GR__ = _mm_slli_epi32(ABGR,16);
__m128i ABab = _mm_blend_epi16(ABGR, __ab, 0x55);
__m128i GRgr = _mm_blend_epi16(GR__, abgr, 0x55);
__m128i A_a_ = _mm_and_si128(ABab, _mm_set1_epi16(0xFF00));
__m128i G_g_ = _mm_and_si128(GRgr, _mm_set1_epi16(0xFF00));
__m128i R_r_ = _mm_slli_epi16(GRgr, 8);
__m128i B_b_ = _mm_slli_epi16(ABab, 8);
// actual alpha-scaling:
__m128i inv = _mm_set1_epi16(0x8081); // = ceil((1<<(16+7))/255.0)
G_g_ = _mm_mulhi_epu16(_mm_mulhi_epu16(G_g_, A_a_), inv);
// shift 7 to the right and 8 to the left, or shift 1 to the left and mask:
G_g_ = _mm_and_si128(_mm_add_epi16(G_g_, G_g_), _mm_set1_epi16(0xFF00));
__m128i _R_r = _mm_mulhi_epu16(_mm_mulhi_epu16(R_r_, A_a_), inv);
_R_r = _mm_srli_epi16(_R_r,7);
__m128i _B_b = _mm_mulhi_epu16(_mm_mulhi_epu16(B_b_, A_a_), inv);
_B_b = _mm_srli_epi16(_B_b,7);
// re-assemble channels:
GRgr = _mm_or_si128(_R_r, G_g_);
ABab = _mm_or_si128(A_a_, _B_b);
__m128i __GR = _mm_srli_epi32(GRgr, 16);
__m128i ab__ = _mm_slli_epi32(ABab, 16);
ABGR = _mm_blend_epi16(ABab, __GR, 0x55);
abgr = _mm_blend_epi16(ab__, GRgr, 0x55);
// store result
_mm_store_si128(input, abgr);
_mm_store_si128(input+1, ABGR);
}
}
Имена переменных используют _
для обозначения 0,и младший байт адреса находится справа (чтобы быть менее запутанным со сдвигом и битовыми операциями).Каждый регистр будет содержать 4 последовательных пикселя или 4 + 4 чередующихся канала.Строчные и прописные буквы находятся в разных местах ввода.(Godbolt: https://godbolt.org/z/OcxAfJ)
В Haswell (или ранее) это было бы узким местом на порту 0 (сдвиг и умножение), но с SSSE3 вы могли бы заменить все 8- и 16-смены на _mm_alignr_epi8
. Ибыло бы лучше оставить _R_r
и _B_b
на младших байтах (использует pand
вместо psllw
, но требует смещения A_a_
на _A_a
). Возможная ловушка: clang заменяет _mm_alignr_epi8
с помощью соответствующих инструкций смены: https://godbolt.org/z/BhEZoV (возможно, существуют флаги, запрещающие лязгу заменять их. GCC использует palignr
: https://godbolt.org/z/lu-jNQ)
На Skylake это может быть оптимальным, как есть (за исключениемконечно, портирование на AVX2. Существует 8 смен, 6 умножений и 1 сложение, т. е. 15 операций на портах 0 и 1. Кроме того, 4 операции на порте 5 и 5 и / или операции (4 на p5 и другая на любомp0 или p1), т. е. 8 моп на порт для 8 пикселей (или 16 пикселей для AVX2).
Код должен быть очень простым для переноса на AVX2 (и использование только AVX1 позволит сохранить некоторые копии регистров)., чтобы сделать код SSE2 совместимым, толькоИнструкции смешивания должны быть заменены соответствующими и + или операциями.