如何使用SSE将16位整数除以255?

时间:2016-02-09 06:28:42

标签: c++ image-processing sse simd sse2

我处理图像处理。 我需要将16位整数SSE向量除以255。

我不能使用像_mm_srli_epi16()那样的移位运算符,因为255不是2的幂的倍数。

我当然知道可以将整数转换为float,执行除法然后返回转换为整数。

但也许有人知道另一种解决方案......

5 个答案:

答案 0 :(得分:14)

有一个除以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);
}

对于NEON(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);
}

答案 1 :(得分:7)

如果您想要针对所有情况获得完全正确的结果,请按照Marc Glisse's对Anton链接问题的评论提出建议:SSE integer division?

使用GNU C本机向量语法来表达给定标量and see what it does on the 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);
}

如果您在前端吞吐量或英特尔CPU上的端口0吞吐量方面遇到瓶颈,那么只有2 uop,这比@ ermlg的答案具有更好的吞吐量(但延迟更差)。 (与往常一样,当您将其用作更大函数的一部分时,它取决于周围的代码。)http://agner.org/optimize/

向量移位仅在英特尔芯片上的端口0上运行,因此@ ermlg的2个移位+ 1在端口0上添加瓶颈。(同样取决于周围的代码)。这是3 uops vs. 2。

在Skylake上,pmulhuw / pmulhw在端口0或1上运行,因此它可以与班次并行运行。 (但是在Broadwell和之前,它们仅在端口0上运行,与移位相冲突。因此,英特尔预Skylake的唯一优势是前端的总uop和跟踪的无序执行。) pmulhuw在英特尔上有5个周期延迟,而在移位时有1个周期延迟,但是当你可以节省微量以获得更多吞吐量时,OoO exec通常可以隐藏几个周期更多的延迟。

Ryzen也只在其P0上运行pmulhuw,但在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足够聪明,可以在内存中为set1使用相同的16B常量,并为div255生成自己的常量。 AFAIK,这就像字符串常量合并一样。

答案 2 :(得分:3)

GCC优化x/255 x unsigned shortDWORD(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)

答案 3 :(得分:2)

准确的版本:

#define div_255_fast(x)    (((x) + (((x) + 257) >> 8)) >> 8)

当x在[0,65536]的范围内时,ERROR为0。它的速度是x/255的两倍:

enter image description here

http://quick-bench.com/t3Y2-b4isYIwnKwMaPQi3n9dmtQ

SIMD版本:

// (x + ((x + 257) >> 8)) >> 8
static inline __m128i _mm_fast_div_255_epu16(__m128i x) {
    return _mm_srli_epi16(_mm_adds_epu16(x, 
        _mm_srli_epi16(_mm_adds_epu16(x, _mm_set1_epi16(0x0101)), 8)), 8);
}

对于大于65535的正x,这是另一个版本:

static inline int32_t fast_div_255_any (int32_t n) {
    uint64_t M = (((uint64_t)1) << 40) / 255 + 1;  // "1/255" in 24.40 fixed point number
    return (M * n) >> 40;   // fixed point multiply: n * (1/255)
}

更广泛(需要64位mul),但仍比div指令快。

答案 4 :(得分:1)

出于好奇(如果存在性能问题),这是使用的准确性 (val + offset)>> 8代替(val / 255),直到所有255 * 255的所有16位值(例如,使用8bit混合因子混合两个8bit值时):

$this->attributes['visible']

所有其他偏移量都会产生较大的有符号和平均误差。因此,如果您的平均误差小于0.25,而不是使用offset + shift进行较小的速度提升

(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