英特尔在故意重叠的内存区域上存储指令

Intel store instructions on delibrately overlapping memory regions

我必须将 YMM 寄存器中较低的 3 个双精度值存储到一个大小为 3 的未对齐双精度数组中(即不能写入第 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 知道他们必须用 __attribute__((may_alias)).

定义 __m128 的一种方式

都在一个线程内,完全同步,所以肯定没有"race condition"。在您的情况下,商店发生的顺序甚至无关紧要(假设它们不与 __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]

所以它被严格定义为首先存储低地址(第二个 arg;我想你搞反了)。


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

你的方式花费 1 次过马路洗牌 + vmovups + vextractf128 [mem], ymm, 1。取决于它的编译方式,直到洗牌之后,这两个商店都不能开始。 (虽然看起来 clang 可能避免了这个问题)。

在 Intel CPU 上,vextractf128 [mem], ymm, imm 前端花费 2 微指令, 微融合成一个。 (出于某种原因在 Zen 上也是 2 微指令。)

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

你只想存储输入向量的低通道和高通道的低元素。最便宜的洗牌是 vextractf128 xmm, ymm, 1 - Zen 上的 1 uop / 1c 延迟(无论如何它将 YMM 向量分成两个 128 位的一半)。它与 Intel 上的任何其他车道交叉洗牌一样便宜。

你要编译器做的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 使蒙面的 loads/stores first-class 公民之前,最好洗牌并进行 2 次存储。或者填充你的数组/结构,这样你至少可以暂时踩到后面的东西。

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

在 Intel Broadwell 和更早版本上,根据 Agner's Fog's 测试,vmaskmov 存储为 4 微指令,因此这比我们从 shuffle + movups + movsd 获得的融合域微指令多 1 个。但是,Haswell 和后来确实管理了 1/clock 吞吐量,所以如果这是一个瓶颈,那么它会超过 2 个商店的 2-cycle 吞吐量。 SnB/IvB 256 位存储当然需要 2 个周期,即使没有屏蔽。

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

所以如果你只关心Skylake及以后的版本,并且可以分摊将掩码加载到向量寄存器的成本(可重复用于加载和存储),vmaskmovpd其实已经很不错了. 前端成本相同但后端成本更低:每个商店地址和商店数据微指令只有 1 个,而不是 2 个单独的商店。 注意 Haswell 和更高版本上的 1/clock 吞吐量与进行 2 个单独存储的 2-cycle 吞吐量。

vmaskmovpd 甚至可以有效地转发到掩码重载;我想英特尔在他们的优化手册中提到了一些关于这个的东西。