c++ - 英特尔存储故意重叠内存区域的指令

标签 c++ intrinsics avx

我必须将 YMM 寄存器中的低 3 个 double 存储到大小为 3 的未对齐 double 数组中(即不能写入第 4 个元素)。但是有点顽皮,我想知道 AVX 内在是否是 _mm256_storeu2_m128d可以做到这一点。我有

reg = _mm256_permute4x64_pd(reg, 0b10010100); // [0 1 1 2]
_mm256_storeu2_m128d(vec, vec + 1, reg);

并由 clang 编译给出
vmovupd xmmword ptr [rsi + 8], xmm1 # reg in ymm1 after perm
vextractf128    xmmword ptr [rsi], ymm0, 1

storeu2具有类似 memcpy 的语义那么它肯定会触发未定义的行为。但是使用生成的指令,这会不会出现竞争条件(或其他潜在问题)?

也欢迎使用其他方式将 YMM 存储到大小为 3 的数组中。

最佳答案

除了英特尔作为文档发布的内容之外,并没有真正针对英特尔的内在函数 AFAIK 的正式规范。例如他们的内在指南。还有他们白皮书中的例子等等;例如需要工作的示例是 GCC/clang 知道他们必须定义的一种方式 __m128__attribute__((may_alias)) .

这一切都在一个线程中,完全同步,所以绝对没有“竞争条件”。在您的情况下,商店发生的顺序甚至无关紧要(假设它们不与 __m256d reg 对象本身重叠!这相当于重叠的 memcpy 问题。)您正在做的可能是两个indeterminately sequenced memcpy 到重叠的目的地:它们肯定以一种或另一种顺序发生,编译器可以选择其中一种。

存储顺序的可观察差异是性能:如果您想在不久之后进行 SIMD 重新加载,那么如果 16 字节重新加载从一个 16 字节存储中获取数据,而不是两个存储的重叠,则存储转发将工作得更好.

不过,一般来说,重叠存储对性能来说很好;存储缓冲区将吸收它们。但是,这意味着其中之一是未对齐的,并且跨越缓存线边界会更昂贵。

然而,这一切都没有实际意义:Intel's intrinsics guide does list an "operation" section for that compound intrinsic :

Operation

MEM[loaddr+127:loaddr] := a[127:0]
MEM[hiaddr+127:hiaddr] := a[255:128]


所以它首先被严格定义为低地址存储(第二个参数;我认为你把这个倒过来了)。

所有这些也没有实际意义,因为有一种更有效的方法

你的方式花费 1 车道交叉洗牌 + vmovups + vextractf128 [mem], ymm, 1 .根据它的编译方式,两个商店都不能在 shuffle 之后启动。 (虽然看起来 clang 可能避免了这个问题)。

在 Intel CPU 上,vextractf128 [mem], ymm, imm前端的成本为 2 uop,而不是微融合为一个。 (出于某种原因,在 Zen 上也有 2 个 uops。)

在 Zen 2 之前的 AMD CPU 上,车道交叉洗牌超过 1 uop,因此 _mm256_permute4x64_pd比必要的贵。

您只想存储输入 vector 的低车道,以及高车道的低元素 .最便宜的shuffle是vextractf128 xmm, ymm, 1 - Zen 上的 1 uop/1c 延迟(无论如何,它将 YMM vector 分成两个 128 位的一半)。它与英特尔上的任何其他过道洗牌一样便宜。

你想让编译器做的 asm 大概就是这个,只需要 AVX1。 AVX2 对此没有任何有用的说明。
    vextractf128  xmm1, ymm0, 1            ; single uop everywhere
    vmovupd       [rdi], xmm0              ; single uop everywhere
    vmovsd        [rdi+2*8], xmm1          ; single uop everywhere

所以你想要这样的东西,它应该可以有效地编译。
    _mm_store_pd(vec, _mm256_castpd256_pd128(reg));  // low half
    __m128d hi = _mm256_extractf128_pd(reg, 1);
    _mm_store_sd(vec+2, hi);
    // or    vec[2] = _mm_cvtsd_f64(hi);
vmovlps ( _mm_storel_pi ) 也可以工作,但是使用 AVX VEX 编码它不会节省任何代码大小,并且需要更多的转换才能让编译器满意。

不幸的是没有 vpextractq [mem], ymm ,仅使用 XMM 源,因此无济于事。

蒙面店:

正如评论中所讨论的,是的,您可以这样做 vmaskmovps但不幸的是,它不像我们在所有 CPU 上所希望的那样高效。直到 AVX512 使蒙面的负载/商店成为一等公民,最好洗牌并做 2 个商店。或者填充你的数组/结构,这样你至少可以暂时踩到后面的东西。

Zen 有 2-uop vmaskmovpd ymm负载,但非常昂贵 vmaskmovpd存储(42 uop,YMM 每 11 个周期 1 个)。或者 Zen+ 和 Zen2 是 18 或 19 uop,6 周期吞吐量。 如果您完全关心禅,请避免 vmaskmov .

在 Intel Broadwell 及更早版本上,vmaskmov根据 Agner's Fog's,商店是 4 uop测试,所以这比我们从 shuffle + movups + movsd 得到的多 1 个融合域 uop。但是,Haswell 和更高版本确实管理 1/clock 吞吐量,因此如果这是瓶颈,那么它会击败 2 个商店的 2 周期吞吐量。对于 256 位存储,SnB/IvB 当然需要 2 个周期,即使没有屏蔽。

在天湖, vmaskmov mem, ymm, ymm is only 3 uops (Agner Fog 列出了 4 个,但他的电子表格是手工编辑的,以前也有错误。我认为可以安全地假设 uops.info 的自动化测试是正确的。这是有道理的;Skylake-client 与 Skylake-AVX512 的核心基本相同, 只是没有实际启用 AVX512。所以他们可以通过将测试解码为掩码寄存器 (1 uop) + 掩码存储(另外 2 uop 没有微融合)来实现 vmaskmovpd

因此,如果您只关心 Skylake 及更高版本,并且可以分摊将掩码加载到 vector 寄存器(可重用于加载和存储)的成本,vmaskmovpd其实还不错。 前端成本相同,但后端更便宜:每个商店地址和商店数据 uop 仅 1 个,而不是 2 个独立的商店。 请注意 Haswell 和更高版本上的 1/clock 吞吐量与执行 2 个独立存储的 2-cycle 吞吐量。
vmaskmovpd甚至可以有效地存储转发到掩码重新加载;我认为英特尔在他们的优化手册中提到了一些关于这一点的内容。

关于c++ - 英特尔存储故意重叠内存区域的指令,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59581140/

相关文章:

c++ - SIMD/SSE : short dot product and short max value

c++ - 无法使用 BitScanForward64

c++ - _mm256_extractf32x4_ps 和 _mm256_extractf128_ps 之间的区别

c++ - 命名空间的 Boost.Log 错误

c++ - 如何使用系统调用 write() 输出到文件(写入文件)?

c++ - 从 .txt 文件中读取 UNICODE 字符并将它们输出到控制台

assembly - cmpeqpd 有时会返回错误的值

C++ : Suggest names for mutating and non-mutating versions of a member function

c - 英特尔至强融核使用的内在函数是否比自动矢量化获得更好的性能?

c++ - 使用 gcc 4.6.1 通过 mex 启用 AVX 指令