【问题标题】:SSE2 packed 8-bit integer signed multiply (high-half): Decomposing a m128i (16x8 bit) into two m128i (8x16 each) and repackSSE2 打包 8 位整数有符号乘法(高半部分):将一个 m128i(16x8 位)分解为两个 m128i(每个 8x16)并重新打包
【发布时间】:2019-12-09 01:08:20
【问题描述】:

我正在尝试将每个字节乘以两个 m128i 字节(8 位有符号整数)。

这里的问题是溢出。我的解决方案是将这些 8 位有符号整数存储为 16 位有符号整数,相乘,然后将整个内容打包成一个 16 x 8 位整数的m128i

这是我制作的__m128i mulhi_epi8(__m128i a, __m128i b) 仿真:

inline __m128i mulhi_epi8(__m128i a, __m128i b)
{
    auto a_decomposed = decompose_epi8(a);
    auto b_decomposed = decompose_epi8(b);

    __m128i r1 = _mm_mullo_epi16(a_decomposed.first, b_decomposed.first);
    __m128i r2 = _mm_mullo_epi16(a_decomposed.second, b_decomposed.second);

    return _mm_packs_epi16(_mm_srai_epi16(r1, 8), _mm_srai_epi16(r2, 8));
}

decompose_epi8以非simd方式实现:

inline std::pair<__m128i, __m128i> decompose_epi8(__m128i input)
{
    std::pair<__m128i, __m128i> result;

    // result.first     =>  should contain 8 shorts in [-128, 127] (8 first bytes of the input)
    // result.second    =>  should contain 8 shorts in [-128, 127] (8 last bytes of the input)

    for (int i = 0; i < 8; ++i)
    {
        result.first.m128i_i16[i]   = input.m128i_i8[i];
        result.second.m128i_i16[i]  = input.m128i_i8[i + 8];
    }

    return result;
}

此代码运行良好。我现在的目标是实现这个 for 循环的 simd 版本。我查看了Intel Intrinsics Guide,但我找不到这样做的方法。我想 shuffle 可以解决问题,但我无法将其概念化。

【问题讨论】:

  • 试试_mm_unpacklo_epi8_mm_unpackhi_epi8
  • @Lawrence 我试试看,谢谢
  • @Lawrence _mm_unpacklo_epi8 对签名解包没有多大帮助。使用 SSE4.1 _mm_cvtepi8_epi16 可能是一种选择。
  • @Lawrence,实际上,_mm_unpack{lo,hi}_epi8 可以工作,因为您可以将数据字节解压缩到每个 16 位字的上半部分(并使用 _mm_mulhi_epi16 进行乘法运算)。我认为您仍然需要移动(或巧妙地置换)乘法结果,因为我认为_mm_packs_epi16 的变体不会向右移动而不是饱和。

标签: c++ intel sse simd intrinsics


【解决方案1】:

当您想要进行有符号乘法时,您需要将每个字节符号扩展为 16 位字,或者将它们移动到每个 16 位字的上半部分。由于您之后将结果打包在一起,因此您可以将输入拆分为奇数和偶数字节,而不是高半部分和低半部分。 然后可以通过算术将所有 16 位部分向右移动来完成奇数字节的符号扩展您可以通过屏蔽偶数字节来提取奇数字节,并获得偶数字节,您可以移位左边的所有 16 位部分(都需要乘以 _mm_mulhi_epi16)。

以下内容应适用于 SSE2:

__m128i mulhi_epi8(__m128i a, __m128i b)
{
    __m128i mask = _mm_set1_epi16(0xff00);
    // mask higher bytes:
    __m128i a_hi = _mm_and_si128(a, mask);
    __m128i b_hi = _mm_and_si128(b, mask);

    __m128i r_hi = _mm_mulhi_epi16(a_hi, b_hi);
    // mask out garbage in lower half:
    r_hi = _mm_and_si128(r_hi, mask);

    // shift lower bytes to upper half
    __m128i a_lo = _mm_slli_epi16(a,8);
    __m128i b_lo = _mm_slli_epi16(b,8);
    __m128i r_lo = _mm_mulhi_epi16(a_lo, b_lo);
    // shift result to the lower half:
    r_lo = _mm_srli_epi16(r_lo,8);

    // join result and return:
    return _mm_or_si128(r_hi, r_lo);
}

注意:以前的版本使用移位来对奇数字节进行符号扩展。在大多数 Intel CPU 上,这会增加 P0 的使用(也需要用于乘法)。位逻辑可以在更多的端口上运行,所以这个版本应该有更好的吞吐量。

【讨论】:

  • 谢谢,真的很有帮助!
  • 不错的更新。同意这看起来像是 mul/shift 与其他 uops 的更好平衡。 Skylake 可以在 p0 或 p1 上运行整数 shift/mul,但是获得更多可以使用 p5 的微指令仍然很好。对 Ryzen 也有好处:即使 mul 和 shift 在不同的端口上运行,它们的吞吐量也只有 1c。 (与可在锐龙 P0123 上运行的 pand/pxor 的 4/时钟吞吐量相比)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-05-11
  • 1970-01-01
  • 1970-01-01
  • 2013-08-29
  • 1970-01-01
相关资源
最近更新 更多