两个256位整数的按位异或运算

11

我有一个不支持AVX2的AVX CPU,我想计算两个256位整数的按位异或。

由于_mm256_xor_si256只适用于AVX2,我是否可以使用_mm256_load_ps将这256位作为__m256加载,然后执行_mm256_xor_ps。这样会产生预期结果吗?

我的主要担忧是如果内存内容不是有效的浮点数,_mm256_load_ps是否无法将位加载到寄存器中与内存中的位完全相同?

谢谢。


1
你尝试过它,发生了什么事? - Andrew Morton
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Kan Li
在这个上下文中,“所有可能的输入”意味着2^32位组合,对于任何现代机器来说并不是什么大问题。当然,最好还是有一个明确的答案(现在已经给出了),而不是仅依赖于蛮力验证。 - void_ptr
2
@void_ptr:你只能在一些特定的硬件模型上进行暴力测试。仅仅因为在你的机器上运行良好而没有任何文档支持推理,就认为某个东西总体上是可以的,这总是一个坏主意。例如,SSE加载/存储超过64b的宽度不保证是原子性的,但在许多机器上它们是原子性的。在Pentium M上,它们被分成两个单独的64b操作。在多插槽Opteron上,它们极少不是原子性的。同样,一些指令恰好不会在SnB上修改标志,但ISA表示它们是未定义的。 - Peter Cordes
另外,我更新了我的答案,指出如果你需要将数据移动到向量寄存器中仅仅是为了进行异或运算,而在你需要将其存回内存之前还需要将其放回整数寄存器以供其他用途使用,那么这样做是不值得的。 - Peter Cordes
3个回答

13
首先,如果您正在处理256b整数的其他操作(例如加/减/乘),仅为偶尔使用XOR而将它们转换为向量寄存器可能不值得传输的开销。如果您已经有两个数字在寄存器中(共使用8个寄存器),则只需要四个指令即可获得结果(如果需要避免覆盖目标,则需要4个指令)。破坏性版本可以在SnB上每1.33个时钟周期运行一次,在Haswell及更高版本上可以每个时钟周期运行一次。(可以在4个ALU端口中的任何一个上运行)。因此,如果您只是在某些或其他操作之间执行单个,请坚持使用整数。

以 64b 为单位存储到内存中,然后进行 128b 或 256b 的加载会 导致存储前传失败,增加几个时钟周期的延迟。使用 movq / pinsrq 将比 xor 更消耗执行资源。反之则不是那么糟糕:256b 存储 -> 64b 加载对于存储前传来说是没问题的。movq / pextrq 仍然很差,但延迟更低(代价是更多的 uops)。


FP加载/存储/位运算在架构上保证不会生成FP异常,即使用于表示信号NaN的位模式。只有实际的FP数学指令列出了数学异常:

VADDPS

SIMD浮点异常
溢出,下溢,无效, 精度,反规范化。

VMOVAPS

SIMD浮点异常
无。

(摘自英特尔指令参考手册。有关此手册及其他内容的链接,请参见 wiki页面。)

在英特尔硬件上,任何一种类型的加载/存储操作都可以在不额外延迟的情况下进入FP或整数域。AMD同样无论使用哪种类型的加载/存储操作,都会表现出相同的行为,不管数据去向/来源如何。

对于寄存器<-寄存器传递,使用不同类型的矢量移动指令实际上很重要。在Intel Nehalem上,使用错误的mov指令可能会导致旁路延迟。而在AMD Bulldozer系列中,由于移动是通过寄存器重命名而不是实际复制数据来处理的(与Intel IvB及更高版本类似),目标寄存器继承了写入源寄存器所在域的属性。

没有我阅读过的任何设计与 movaps 不同地处理 movapd。据推测,英特尔创建 movapd 的原因既是为了解码简单性,也是为了未来规划(例如允许存在双域和单域设计,具有不同的转发网络)。(movapd 就像每个其他 SSE 指令的双精度版本一样,只是添加了 66h 前缀字节。或者对于标量指令,使用 F2 而不是 F3。)
显然,AMD 设计将 FP 向量标记为辅助信息,因为 Agner Fog 发现 例如在使用 addps 的输出作为 addpd 的输入时产生了大延迟。但我不认为两个 addpd 指令之间甚至是 xorps 指令会导致该问题,只有实际的 FP 数学运算才会引起问题。(FP 位布尔运算对于 Bulldozer 家族而言是整数域。)

在只拥有AVX而没有AVX2的Intel SnB/IvB上的理论吞吐量:

使用AVX xorps 进行256b操作

VMOVDQU   ymm0, [A]
VXORPS    ymm0, ymm0, [B]
VMOVDQU   [result], ymm0
  • 由于流水线宽度为4个融合域uop,因此每0.75个周期可以发出3个融合域uop(假设您用于B和result的寻址模式可以微调,否则为5个融合域uop)。

  • 加载端口:SnB上的256b加载/存储需要2个周期(分成128b的两半),但这会释放出端口2/3上的AGU以用于存储。其中有一个专门的存储数据端口,但存储地址计算需要来自加载端口的AGU。

    因此,仅使用128b或更小的加载/存储,SnB/IvB可以每个周期维持两个内存操作(最多其中一个是存储)。对于256b的操作,SnB/IvB理论上可以每两个周期支持两个256b加载和一个256b存储。然而,缓存银行冲突通常使其不可能实现。

    Haswell具有专用的存储地址端口,并且可以每个周期支持两个256b加载和一个256b存储。并且不会存在缓存银行冲突。所以,当所有东西都在L1高速缓存中时,Haswell要快得多。

底线是: 理论上(没有缓存冲突),这应该可以饱和SnB的加载和存储端口,每个周期处理128b。端口5(唯一可以运行xorps指令的端口)每两个时钟周期需要使用一次。

128b操作

VMOVDQU   xmm0, [A]
VMOVDQU   xmm1, [A+16]
VPXOR     xmm0, xmm0, [B]
VPXOR     xmm1, xmm1, [B+16]
VMOVDQU   [result],    xmm0
VMOVDQU   [result+16], xmm1

这将在地址生成上成为瓶颈,因为SnB每个周期只能维持两个128b内存操作。它还会使用2倍的uop缓存空间和更多的x86机器代码大小。除了缓存冲突,这应该以每3个时钟周期一个256b-xor的吞吐量运行。

在寄存器中

在寄存器之间,每个时钟周期一个256b的VXORPS和两个128b的VPXOR会使SnB饱和。 在Haswell上,每个时钟周期三个AVX2 256b的VPXOR将提供最多的异或操作。 (XORPSPXOR执行相同的操作,但是XORPS的输出可以直接转发到FP执行单元,而不需要额外的转发延迟周期。 我猜只有一个执行单元具有将XOR结果转换为FP域的布线,因此Intel Nehalem之后的CPU只在一个端口上运行XORPS。)


Z玻色子的混合理念:

VMOVDQU   ymm0, [A]
VMOVDQU   ymm4, [B]
VEXTRACTF128 xmm1, ymm0, 1
VEXTRACTF128 xmm5, ymm1, 1
VPXOR     xmm0, xmm0, xmm4
VPXOR     xmm1, xmm1, xmm5
VMOVDQU   [res],    xmm0
VMOVDQU   [res+16], xmm1

比只做128b一切更多的融合域uops(8)。
加载/存储:两个256b加载留下两个空闲周期以生成两个存储地址,因此这仍然可以以每个周期2个128b的负载/一个存储运行。
ALU:两个端口-5 uops(vextractf128),两个端口0/1/5 uops(vpxor)。
因此,这仍然具有每2个时钟周期一个256b结果的吞吐量,但它饱和了更多资源,在Intel上没有优势超过3指令256b版本。

1
很棒的回答!汇编代码看起来非常不错。有趣的是,它比内置函数更好看。我有点惊讶我的混合方法比纯SSE版本的吞吐量更好。 - Z boson
你可能需要考虑编辑你的回答,包括这个答案:https://dev59.com/M2w15IYBdhLWcg3wVaQa - Z boson
@Zboson:是的,这不是个坏主意。汇编看起来非常整洁,因为我没有包括任何关于输入的内容,并且使用了占位符A、B和result,而不是有关哪个寄存器指向哪里的位注释。ASM助记符比用于Intrinsic的超长名称要容易阅读得多。大多数时候,Intrinsic名称很糟糕。它们要长得多,但仍然有需要解码的位(如epu8 vs. epi32)。而要获得关于它们确切执行的完整信息,您需要按asm助记符查找它们。我会更喜欢 _mm_pshufb(...) - Peter Cordes
也许他们想把Byte、Word、Dword、Qword这些东西从C语言中剔除,因为在32位/64位机器(如x86)上,“word”是16位,这会让人们感到困惑。 - Peter Cordes

3

使用_mm256_load_ps加载整数没有问题。事实上,在这种情况下,它比使用_mm256_load_si256更好(后者确实可以使用AVX),因为使用_mm256_load_ps可以保持在浮点数域内。

#include <x86intrin.h>
#include <stdio.h>

int main(void) {
    int a[8] = {1,2,3,4,5,6,7,8};
    int b[8] = {-2,-3,-4,-5,-6,-7,-8,-9};

    __m256 a8 = _mm256_loadu_ps((float*)a);
    __m256 b8 = _mm256_loadu_ps((float*)b);
    __m256 c8 = _mm256_xor_ps(a8,b8);
    int c[8]; _mm256_storeu_ps((float*)c, c8);
    printf("%x %x %x %x\n", c[0], c[1], c[2], c[3]);
}

如果你想保持整数领域,你可以这样做:

#include <x86intrin.h>
#include <stdio.h>

int main(void) {
    int a[8] = {1,2,3,4,5,6,7,8};
    int b[8] = {-2,-3,-4,-5,-6,-7,-8,-9};

    __m256i a8 = _mm256_loadu_si256((__m256i*)a);
    __m256i b8 = _mm256_loadu_si256((__m256i*)b);
    __m128i a8lo = _mm256_castsi256_si128(a8);
    __m128i a8hi = _mm256_extractf128_si256(a8, 1);
    __m128i b8lo = _mm256_castsi256_si128(b8);
    __m128i b8hi = _mm256_extractf128_si256(b8, 1);
    __m128i c8lo = _mm_xor_si128(a8lo, b8lo);
    __m128i c8hi = _mm_xor_si128(a8hi, b8hi);
    int c[8];
    _mm_storeu_si128((__m128i*)&c[0],c8lo);
    _mm_storeu_si128((__m128i*)&c[4],c8hi);
    printf("%x %x %x %x\n", c[0], c[1], c[2], c[3]);
}
_mm256_castsi256_si128 内联函数是免费的。

据我所知,英特尔CPU在使用整数加载-> FP指令或反之亦无惩罚。我忘记了AMD CPU是否有。但是,在AMD CPU上,“xor_ps”在整数域中运行。 - Peter Cordes
@PeterCordes,如果没有惩罚的话,那为什么要有整数和浮点数的加载指令呢?可以只有一种无类型的加载指令。 - Z boson
当设计时,他们可能考虑到了延迟更少的设计可能性。实际上,他们最终制造出了CPU,两种负载具有相等的延迟。AMD也是如此:根据Agner Fog的微架构指南,Bulldozer系列CPU从load-domain到FP-domain或ivec-domain具有相同的6c延迟。存储器也不会受到使用何种存储指令的影响。这可能是因为没有很好地规划未来。我不知道为什么他们在SSE2中引入了movdqa,而不是只保留movaps - Peter Cordes
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Peter Cordes
@PeterCordes,非常好的链接,谢谢!但是单精度和双精度浮点加载指令有什么意义呢? - Z boson

1

你可能会发现,使用 _mm256_xor_ps 和使用 2 x _mm_xor_si128 相比,性能几乎没有区别。甚至有可能 AVX 实现会更慢,因为在 SB/IB/Haswell 上,_mm256_xor_ps 的倒数吞吐量为1,而 _mm_xor_si128 的倒数吞吐量为0.33。


@icando:是的,0.33表示每个时钟周期最多可以执行三条指令,因此理论上您的两个 _mm_xor_si128 指令应该能够在同一时钟周期内执行,前提是没有其他依赖关系。(请参阅Agner Fog的"Instruction Tables"第3页,即Reciprocal Throughput)。 - Paul R
2
问题在于,如果我使用256位指令,需要两次加载,一次_mm256_xor_ps,一次存储,而如果我使用128位指令,则需要四次加载,两次_mm_xor_si128和两次存储。_mm_xor_si128比_mm256_xor_ps更快的好处可能会被这些更多的加载和存储所抵消。我仍然对我的原始问题感兴趣。 - Kan Li
2
请注意,256位操作在前几千个时钟周期内可能会很慢,直到CPU决定停止模拟它们并将执行单元的上半部分退出省电模式或其他模式。请参阅http://www.agner.org/optimize/blog/read.php?i=142#378,了解有关256位操作预热时间的讨论。 - Peter Cordes
1
@icando:你说的可能是对的,但更多的指令并不一定意味着更慢——尝试两种方式并进行基准测试将会很有趣,但我怀疑差别不大。 - Paul R
2
@PaulR:请看我的回答:如果需要加载/存储,则每2个周期只需要在端口5上使用一次256b XOR单元,因此远未达到饱和。即使其中一个值已经加载到内存中,或者您不需要存储结果,您也不太可能在端口5上出现瓶颈。如果许多值在寄存器中保持活动状态,则混合使用256b xorps和128b pxor可能很好,但解包/重新打包不值得。无论如何,我认为我现在只是在重复自己。 >.< 你说得对,OP实际用例的基准测试是决定的方法。 - Peter Cordes
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接