C++ 英特尔在反复重叠的内存区域上存储指令

C++ 英特尔在反复重叠的内存区域上存储指令,c++,intrinsics,avx,C++,Intrinsics,Avx,我必须将YMM寄存器中较低的3个双精度存储到大小为3的未对齐双精度数组中(即,无法写入第4个元素)。但是有点淘气,我想知道AVX内在的\u mm256\u storeu2\u m128d是否能做到这一点。我有 reg = _mm256_permute4x64_pd(reg, 0b10010100); // [0 1 1 2] _mm256_storeu2_m128d(vec, vec + 1, reg); 通过叮当声编译 vmovupd xmmword ptr [rsi + 8], xmm1

我必须将YMM寄存器中较低的3个双精度存储到大小为3的未对齐双精度数组中(即,无法写入第4个元素)。但是有点淘气,我想知道AVX内在的
\u mm256\u storeu2\u m128d
是否能做到这一点。我有

reg = _mm256_permute4x64_pd(reg, 0b10010100); // [0 1 1 2]
_mm256_storeu2_m128d(vec, vec + 1, reg);
通过叮当声编译

vmovupd xmmword ptr [rsi + 8], xmm1 # reg in ymm1 after perm
vextractf128    xmmword ptr [rsi], ymm0, 1
如果
storeu2
具有类似
memcpy
的语义,那么它肯定会触发未定义的行为。但有了生成的指令,这会不会没有竞争条件(或其他潜在问题)


其他将YMM存储到大小为3的阵列中的方法也受到欢迎。

除了英特尔发布的文档之外,英特尔的内部芯片AFAIK还没有正式的规范。e、 他们的内在指南。还有他们白皮书中的例子等等;e、 g.需要工作的示例是GCC/clang知道他们必须使用
\uuuu属性((may_别名))
定义
\uuuu m128

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

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

不过,一般来说,重叠存储对于性能来说是不错的;存储缓冲区将吸收它们。但这意味着其中一个未对齐,跨越缓存线边界的成本更高


然而,这都是毫无意义的:

操作

MEM[loaddr+127:loaddr] := a[127:0]
MEM[hiaddr+127:hiaddr] := a[255:128]
所以它严格定义为低地址存储优先(第二个参数;我认为这是反向的)


所有这些都没有意义,因为有一种更有效的方法 您的路线需要花费1车道交叉洗牌+vmovups+
vextractf128[mem],ymm,1
。根据编译方式的不同,这两个存储都不能在洗牌后启动。(尽管看起来叮当声可能避免了这个问题)

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

在Zen 2之前的AMD CPU上,车道交叉混洗超过1 uop,因此
\u mm256\u permute4x64\u pd
比必要的成本更高

您只需要存储输入向量的低端车道和高端车道的低端元素。最便宜的洗牌是Zen上的vextractf128 xmm,ymm,1-1 uop/1c延迟(它将ymm向量分成两个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
\u mm\u storel\u 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 uops,6个周期的吞吐量如果你关心禅宗,就避免使用
vmaskmov

在Intel Broadwell和更早版本上,
vmaskmov
存储根据测试为4个uop,因此这比我们从shuffle+movups+movsd获得的融合域uop多1个。但是,Haswell和后来的公司确实管理1/时钟的吞吐量,因此如果这是一个瓶颈,那么它将超过2个商店的2周期吞吐量。当然,即使没有屏蔽,SnB/IvB对于256位存储也需要2个周期

在天湖,(Agner Fog列出了4个,但他的电子表格是手工编辑的,以前是错误的。我认为假设uops是安全的。info的自动测试是正确的。这是有道理的;Skylake客户端与Skylake-AVX512基本上是同一个核心,只是没有实际启用AVX512。因此他们可以通过将其解码为tes来实现
vmaskmovpd
)t进入屏蔽寄存器(1个uop)+屏蔽存储(2个以上uop,无微熔合)

因此,如果您只关心Skylake和更高版本,并且可以分摊将掩码加载到向量寄存器的成本(可用于加载和存储),
vmaskmovpd
实际上相当不错。
相同的前端成本,但后端更便宜:每个存储地址和存储数据UOP只有1个,而不是2个单独的存储。注意Haswell和更高版本的1/时钟吞吐量,而不是2个单独存储的2周期吞吐量。

    _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);

vmaskmovpd
甚至可以有效地向前存储到屏蔽重新加载;我想英特尔在他们的优化手册中提到了这一点。

如果它有时生成不同的指令,它可能仍然是未定义的行为。@user253751这是真的,但在这种情况下,我可以将我的内部函数更改为
\u mm\u storeu pd
and
extractf128\u pd
甚至使用内联asm。那么
\u mm256\u maskstore\u pd
呢?@chtz