【问题标题】:How to copy X bytes or bits from an __m128i into standard memory如何将 X 字节或位从 __m128i 复制到标准内存中
【发布时间】:2020-12-18 20:22:32
【问题描述】:

我有一个循环,它通过_mm_add_epi16() 将两个数组中的 int16s 添加在一起。有一个小数组和一个大数组,结果被写回大数组。 如果小数组到达终点,内在函数可能会从小数组中获得少于 8 倍的 int16s(128 位) - 当我不想要它的所有 128 位时,如何将 _mm_add_epi16() 的结果存储回标准内存 int16_t*?将阵列填充到二次幂不是一种选择。示例:

int16_t* smallArray;
int16_t* largeArray;
__m128i inSmallArray = _mm_load_si128((__m128i*)smallArray);
__m128i* pInLargeArray = (__m128i*)largeArray;
__m128i inLargeArray = _mm_load_si128(pInLargeArray);
inLargeArray = _mm_add_epi16(inLargeArray, inSmallArray);
_mm_store_si128(pInLargeArray, inLargeArray);

我的猜测是我需要以某种方式将_mm_store_si128() 替换为“蒙面”商店。

【问题讨论】:

  • 您可以直接处理其元素。那是因为 __m128i 是一个联合体。
  • 宽度是任何代码路径上的编译时常量,还是易于分支?像movqmovd 这样的指令可以存储8 或4 个字节。或者,如果 smallArray 的长度至少为 16,则可以执行未对齐的最终向量,如果您安排循环以将结果留在变量中以存储下一次迭代(或离开循环时,在加载可能重叠的未对齐数据后最终向量)。
  • @PeterCordes 宽度是在运行时确定的,我需要 16 位粒度。我也许可以保证 smallArray 至少为 16 个字节 - 你能详细说明未对齐的最终向量是什么意思吗?谢谢!

标签: c++ sse simd intrinsics sse2


【解决方案1】:

有一个 _mm_maskmoveu_si128 内在函数,它转换为 maskmovdqu(在 SSE 中)或 vmaskmovdqu(在 AVX 中)。

// Store masks. The highest bit in each byte indicates the byte to store.
alignas(16) const unsigned char masks[16][16] =
{
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 }
};

void store_n(__m128i mm, unsigned int n, void* storage)
{
    assert(n < 16u);
    _mm_maskmoveu_si128(mm, reinterpret_cast< const __m128i& >(masks[n]), static_cast< char* >(storage));
}

这段代码的问题在于maskmovdqu(可能还有vmaskmovdqu)指令具有对目标内存的非临时访问的相关提示,这使得指令成本很高,并且之后还需要栅栏。

AVX 添加了新指令vmaskmovps/vmaskmovpd(并且 AVX2 还添加了 vpmaskmovd/vpmaskmovq),其工作方式与 vmaskmovdqu 类似,但没有非时间提示,仅对 32 和64 位粒度。

// Store masks. The highest bit in each 32-bit element indicates the element to store.
alignas(16) const unsigned char masks[4][16] =
{
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00 }
};

void store_n(__m128i mm, unsigned int n, void* storage)
{
    assert(n < 4u);
    _mm_maskstore_epi32(static_cast< int* >(storage), reinterpret_cast< const __m128i& >(masks[n]), mm);
}

AVX-512 添加了掩码存储,您可以使用 vmovdqu8/vmovdqu16 和适当的掩码来存储 8 位或 16 位元素。

void store_n(__m128i mm, unsigned int n, void* storage)
{
    assert(n < 16u);
    _mm_mask_storeu_epi8(storage, static_cast< __mmask16 >((1u << n) - 1u), mm);
}

请注意,以上需要 AVX-512BW 和 VL 扩展。

如果您需要 8 位或 16 位粒度并且没有 AVX-512,那么您最好使用手动逐个存储向量寄存器的功能。

void store_n(__m128i mm, unsigned int n, void* storage)
{
    assert(n < 16u);

    unsigned char* p = static_cast< unsigned char* >(storage);
    if (n >= 8u)
    {
        _mm_storel_epi64(reinterpret_cast< __m128i* >(p), mm);
        mm = _mm_unpackhi_epi64(mm, mm); // move high 8 bytes to the low 8 bytes
        n -= 8u;
        p += 8;
    }

    if (n >= 4u)
    {
        std::uint32_t data = _mm_cvtsi128_si32(mm);
        std::memcpy(p, &data, sizeof(data)); // typically generates movd
        mm = _mm_srli_si128(mm, 4);
        n -= 4u;
        p += 4;
    }

    if (n >= 2u)
    {
        std::uint16_t data = _mm_extract_epi16(mm, 0); // or _mm_cvtsi128_si32
        std::memcpy(p, &data, sizeof(data));
        mm = _mm_srli_si128(mm, 2);
        n -= 2u;
        p += 2;
    }

    if (n > 0u)
    {
        std::uint32_t data = _mm_cvtsi128_si32(mm);
        *p = static_cast< std::uint8_t >(data);
    }
}

【讨论】:

  • 我认为vmaskmovdqu 具有相同的 NT 语义;我认为只有 AVX vmaskmovps 和具有 4 或 8 字节粒度的相关指令才能进行普通屏蔽存储(直到 AVX512BW vmaskmovdqu8
  • @PeterCordes Intel SDM 仅记录maskmovdqu 的非临时提示,而不记录vmaskmovdqu。您是否有同样适用于vmaskmovdqu 的参考资料?
  • @PeterCordes 我在 AMD APM 中找到了maskmovdqu/vmaskmovdqu 的描述,那里的描述确实表明两条指令都有非临时提示。我已经更新了我的答案。谢谢。 PS:我找不到vmaskmovdqu8的描述,所以我不能说什么。
  • 那是脑残,AVX512 对普通存储有屏蔽,这就是为什么所有向量 mov 指令都有一个元素大小作为助记符的一部分。我的意思是vmovdqu8 [rdi]{k1}, xmm0felixcloutier.com/x86/…
  • Re: Intel 的文档:在Intel docs 中 VEX 编码是否有 NT 提示是模棱两可的,但如果我们将“MASKMOVDQU”表示任一编码,我认为它们的措辞与现实兼容相同的指令。我认为当 VEX 和旧版 SSE 版本工作相同时,英特尔文档只谈论指令的基本名称是正常的。不过,如果文档更明确一些,那就太好了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-04-13
  • 1970-01-01
  • 2017-02-14
  • 1970-01-01
  • 2018-05-29
  • 2017-01-25
  • 1970-01-01
相关资源
最近更新 更多