Как разделить 16-разрядное целое число на 255 с помощью SSE?
Я занимаюсь обработкой изображений. Мне нужно разделить 16-битный целочисленный вектор SSE на 255.
Я не могу использовать оператор shift как _mm_srli_epi16(), потому что 255 не кратно мощности 2.
Я знаю, конечно, что можно преобразовать integer в float, выполнить деление, а затем обратно преобразование в integer.
но может кто-то знает другое решение...
4 ответов
существует целочисленное приближение деления на 255:
inline int DivideBy255(int value)
{
return (value + 1 + (value >> 8)) >> 8;
}
поэтому с использованием SSE2 это будет выглядеть так:
inline __m128i DivideI16By255(__m128i value)
{
return _mm_srli_epi16(_mm_add_epi16(
_mm_add_epi16(value, _mm_set1_epi16(1)), _mm_srli_epi16(value, 8)), 8);
}
за поддержкой AVX2:
inline __m256i DivideI16By255(__m256i value)
{
return _mm256_srli_epi16(_mm256_add_epi16(
_mm256_add_epi16(value, _mm256_set1_epi16(1)), _mm256_srli_epi16(value, 8)), 8);
}
Для Altivec (Мощность):
typedef __vector int16_t v128_s16;
const v128_s16 K16_0001 = {1, 1, 1, 1, 1, 1, 1, 1};
const v128_s16 K16_0008 = {8, 8, 8, 8, 8, 8, 8, 8};
inline v128_s16 DivideBy255(v128_s16 value)
{
return vec_sr(vec_add(vec_add(value, K16_0001), vec_sr(value, K16_0008)), K16_0008);
}
для неона (ARM):
inline int16x8_t DivideI16By255(int16x8_t value)
{
return vshrq_n_s16(vaddq_s16(
vaddq_s16(value, vdupq_n_s16(1)), vshrq_n_s16(value, 8)), 8);
}
если вы хотите точно правильный результат для всех случаев, следуйте советам из Марк Глисс комментарий на вопрос Антона связан:SSE целочисленное деление?
используйте синтаксис собственного вектора GNU C, чтобы выразить деление вектора на данный скаляр,и посмотреть, что он делает на godbolt compiler explorer:
беззнаковое деление дешевая:
typedef unsigned short vec_u16 __attribute__((vector_size(16)));
vec_u16 divu255(vec_u16 x){ return x/255; } // unsigned division
#gcc5.5 -O3 -march=haswell
divu255:
vpmulhuw xmm0, xmm0, XMMWORD PTR .LC3[rip] # _mm_set1_epi16(0x8081)
vpsrlw xmm0, xmm0, 7
ret
встроенные функции версия:
// UNSIGNED division with intrinsics
__m128i div255_epu16(__m128i x) {
__m128i mulhi = _mm_mulhi_epu16(x, _mm_set1_epi16(0x8081));
return _mm_srli_epi16(mulhi, 7);
}
At только 2 uops, это имеет лучшую пропускную способность (но худшую задержку), чем ответ @ermlg, если вы ограничены пропускной способностью переднего плана или пропускной способностью порта 0 на процессорах Intel. (Как всегда, это зависит от окружающего кода, когда вы используете его как часть большей функции.) http://agner.org/optimize/
Vector shift работает только на Порту 0 на чипах Intel ,поэтому 2 смены @ ermlg + 1 добавляют узкие места на Порту 0. (Опять же в зависимости от окружающего кода). И это 3 uops против 2 для этот.
На Skylake, В pmulhuw
/ pmulhw
работает на порты 0 и 1, поэтому он может работать параллельно со сдвигом. (Но на Broadwell и ранее они работают только на Порту 0, конфликтуя со сдвигами. Таким образом, единственным преимуществом Intel pre-Skylake является меньшее количество общих uops для front-end и для внепланового выполнения.) pmulhuw
имеет задержку цикла 5 на Intel, против 1 для смен, но OoO exec обычно может скрыть несколько циклов больше задержки, когда вы можете сохранить uops для большего пропускная способность.
Ryzen также работает только pmulhuw на своем P0, но сдвигается на P2, поэтому он отлично подходит для этого.
но подпись семантика округления целочисленного деления не соответствует сдвигам
typedef short vec_s16 __attribute__((vector_size(16)));
vec_s16 div255(vec_s16 x){ return x/255; } // signed division
; function arg x starts in xmm0
vpmulhw xmm1, xmm0, XMMWORD PTR .LC3[rip] ; a vector of set1(0x8081)
vpaddw xmm1, xmm1, xmm0
vpsraw xmm0, xmm0, 15 ; 0 or -1 according to the sign bit of x
vpsraw xmm1, xmm1, 7 ; shift the mulhi-and-add result
vpsubw xmm0, xmm1, xmm0 ; result += (x<0)
.LC3:
.value -32639
.value -32639
; repeated
рискуя раздуть ответ, здесь он снова с внутреннеприсущими:
// SIGNED division
__m128i div255_epi16(__m128i x) {
__m128i tmp = _mm_mulhi_epi16(x, _mm_set1_epi16(0x8081));
tmp = _mm_add_epi16(tmp, x); // There's no integer FMA that's usable here
x = _mm_srai_epi16(x, 15); // broadcast the sign bit
tmp = _mm_srai_epi16(tmp, 7);
return _mm_sub_epi16(tmp, x);
}
в выходе godbolt обратите внимание, что gcc достаточно умен, чтобы использовать ту же константу 16B в памяти для set1
и для того, который он создал сам для div255
. AFAIK, это работает как слияние строк-констант.
GCC оптимизирует x/255
С x и unsigned short
to DWORD(x * 0x8081) >> 0x17
который может быть упрощен до HWORD(x * 0x8081) >> 7
и наконец HWORD((x << 15) + (x << 7) + x) >> 7
.
макросы SIMD могут выглядеть следующим образом:
#define MMX_DIV255_U16(x) _mm_srli_pi16(_mm_mulhi_pu16(x, _mm_set1_pi16((short)0x8081)), 7)
#define SSE2_DIV255_U16(x) _mm_srli_epi16(_mm_mulhi_epu16(x, _mm_set1_epi16((short)0x8081)), 7)
#define AVX2_DIV255_U16(x) _mm256_srli_epi16(_mm256_mulhi_epu16(x, _mm256_set1_epi16((short)0x8081)), 7)
из любопытства (и если производительность является проблемой), вот это точность, используя (val + offset) >> 8 в качестве замены (val / 255) для всех 16-битных значений до 255*255 (например, при смешивании двух 8-битных значений с использованием 8-битного коэффициента смешивания):
(avrg signed error / avrg abs error / max abs error)
offset 0: 0.49805 / 0.49805 / 1 (just shifting, no offset)
offset 0x7F: 0.00197 / 0.24806 / 1
offest 0x80: -0.00194 / 0.24806 / 1
все остальные смещения приводят к большим ошибкам signed и avrg. Поэтому, если вы можете жить со средней ошибкой менее 0,25, чем использовать смещение+сдвиг для небольшого увеличения скорости
// approximate division by 255 for packs of 8 times 16bit values in vals_packed
__m128i offset = _mm_set1_epi16(0x80); // constant
__m128i vals_packed_offest = _mm_add_epi16( vals_packed, offset );
__m128i result_packed = _mm_srli_epi16( vals_packed_offest , 8 );