【问题标题】:How to divide 16-bit integer by 255 with using SSE?如何使用 SSE 将 16 位整数除以 255?
【发布时间】:2023-03-09 10:11:01
【问题描述】:

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

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

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

但是可能有人知道另一种解决方案...

【问题讨论】:

  • this 有帮助吗?
  • 通常你会除以 256(四舍五入而不是截断) - 为什么它必须是 255 而不是 256 有什么原因吗?
  • 也许this 的问题对你来说也很有趣。当您将来必须处理非常量整数除法时,转换为浮点数也是一个快速的选择。
  • @Paul-R 如果我将除以 256,当此操作执行多次时,它会导致平均亮度降低。
  • @Claudio:很难说没有看到其余代码,但是如果您使用完整的 16 位范围然后缩小到 8 位,那么比例因子应该是 256 , 否则如果你采取例如满量程 uint16_t = 65535 除以 255 得到 257。我想你知道你在做什么 - 我只是感到困惑......

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


【解决方案1】:

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

【讨论】:

  • 这对于value == 65535 和所有负数都是错误的(因此不适用于有符号和无符号的 16 位整数)
  • 我知道它非常适合 Alpha 混合。但我不排除其他情况下的错误。
  • @AntonSavin:准确无符号除以 255 比使用 pmulhuw 更有效地 获得更好的吞吐量,但延迟更短。几年前我的回答是只看签名,而不是未签名。 (由于某种原因,似乎没有人区分这两者,有时使用int,有时使用 SIMD 逻辑右移。)
【解决方案2】:

GCC 优化 x/255xunsigned 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】:

    如果您想要所有情况下的完全正确的结果,请遵循 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 微秒,比 @ermlg 的答案具有更好的吞吐量(但延迟更差)。 (与往常一样,当您将其用作较大函数的一部分时,它取决于周围的代码。)http://agner.org/optimize/

    向量移位仅在英特尔芯片上的端口 0 上运行,因此@ermlg 的 2 次移位 + 1 在端口 0 上添加了瓶颈。(再次取决于周围的代码)。这是 3 微秒,而 2 微秒。

    在 Skylake 上,pmulhuw / pmulhw 在端口 0 或 1 上运行,因此它可以与班次并行运行。 (但在 Broadwell 和更早的版本上,它们仅在端口 0 上运行,与班次相冲突。因此,英特尔在 Skylake 之前的唯一优势是前端的总 uops 和跟踪的乱序执行较少。) pmulhuw 在 Intel 上具有 5 个周期的延迟,而在轮班时为 1 个,但是当您可以节省 uops 以提高吞吐量时,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 和它自己为 div255 生成的那个使用相同的 16B 常量。 AFAIK,这就像字符串常量合并。

    【讨论】:

      【解决方案4】:

      出于好奇(如果性能是一个问题),这里是使用的准确性 (val + offset) >> 8 作为 (val / 255) 的替代,用于所有 255*255 以内的 16 位值(例如,当使用 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
      

      所有其他偏移都会产生更大的有符号和平均误差。因此,如果您可以忍受小于 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 );
      

      【讨论】:

      • 使用饱和添加,您可以使用它而不会为更接近环绕点的值带来灾难性的结果。 _mm_adds_epi16epu16 用于有符号/无符号饱和。您确定要使用srli(逻辑右移)而不是算术右移srai?还是您这样做是为了未签名?
      【解决方案5】:

      准确版:

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

      x在[0, 65536]范围内时,ERROR为0,比x/255快一倍:

      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 指令快。

      【讨论】:

      • _const_simd_mask_8x0101 是什么?如果这是一个全局变量,编译器通常会比_mm_set1_epi16(0x0101)更糟
      • 另外,我认为您的意思是 [0, 65535][0, 65536),因为 65536 = 2^16 不适合 epu16
      • @PeterCordes,已更新,[0, 65536] 适用于 c 版本。
      • 在纯 C 中用于标量,您不需要特殊技巧。编译器已经知道如何使用乘法逆:Why does GCC use multiplication by a strange number in implementing integer division?。只需使用unsigned,您就可以获得高效的asm,或者像您的签名版本一样适用于任何int32_t:godbolt.org/z/cTzYzb 的asm。您的 fast_div_255_any 似乎可以处理符号位,因此它实际上适用于任何 x,而不仅仅是正数,对吗?我想这是使用_mm_mul_epu32 (pmuludq) 将其转换为 SIMD 的有用步骤
      • @PeterCordes,div_255_fast 比 fast_div_255_any(gcc 可能使用)便宜得多。如果你的数字在 0 到 65536 之间,它仍然比编译器快。
      猜你喜欢
      • 2017-05-24
      • 2014-07-03
      • 2014-03-29
      • 1970-01-01
      • 2019-03-06
      • 2014-11-06
      • 2015-03-27
      • 2018-09-01
      相关资源
      最近更新 更多