两个 256 位整数的按位异或

Bitwise xor of two 256-bit integers

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

由于 _mm256_xor_si256 仅在 AVX2 上可用,我可以使用 _mm256_load_ps 将这 256 位加载为 __m256,然后执行 _mm256_xor_ps。这会产生预期的结果吗?

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

谢谢。

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

使用_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 内在函数是免费的。

首先,如果您正在用 256b 整数做其他事情(如 adding/subtracting/multiplying),将它们放入向量寄存器只是为了偶尔的 XOR 可能不值得传输它们的开销。如果寄存器中已有两个数字(总共使用 8 个寄存器),则只需 4 个 xor 指令即可获得结果(如果需要避免覆盖目标,则需要 4 mov 指令)。破坏性版本可以 运行 在 SnB 上每 1.33 个时钟周期一个,或者在 Haswell 和更高版本上每个时钟一个。 (xor 可以在 4 个 ALU 端口中的任何一个上 运行)。所以如果你只是在一些 add/adc 之间做一个 xor 或其他什么,坚持使用整数。

以 64b 块存储到内存,然后进行 128b 或 256b 加载会 cause a store-forwarding failure,再增加几个延迟周期。使用 movq / pinsrq 会比 xor 消耗更多的执行资源。走另一条路并没有那么糟糕:256b store -> 64b loads 适合存储转发。 movq / pextrq 仍然很糟糕,但延迟会更低(以更多微指令为代价)。


FP load/store/bitwise 操作在体系结构上保证不会生成 FP 异常,即使在表示信号 NaN 的位模式上使用时也是如此。只有实际的 FP 数学指令列出数学异常:

VADDPS

SIMD Floating-Point Exceptions
Overflow, Underflow, Invalid, Precision, Denormal.

VMOVAPS

SIMD Floating-Point Exceptions
None.

(来自英特尔的 insn ref 手册。请参阅 wiki 以获取指向该内容和其他内容的链接。)

在 Intel 硬件上,load/store 中的任何一种都可以在没有额外延迟的情况下进入 FP 或整数域。 AMD 的行为与使用 load/store 的任何一种类似,无论数据来自哪里/来自哪里。

向量移动指令的不同风格actually matter for register<-register moves。在 Intel Nehalem 上,使用错误的 mov 指令会导致旁路延迟。在 AMD Bulldozer 系列上,移动是通过寄存器重命名而不是实际复制数据(如 Intel IvB 和更高版本)处理的,dest 寄存器继承了写入 src 寄存器的域。

我读过的现有设计在处理 movapdmovaps 时没有任何不同。据推测,英特尔创建 movapd 既是为了简化解码,也是为了未来的规划(例如,允许设计具有双域和单域、具有不同转发网络的可能性)。 (movapd 是带有 66h 前缀的 movaps,就像每个其他 SSE 指令的双重版本只是附加了 66h 前缀字节。或者 F2而不是标量指令的 F3。)

显然 AMD 设计了带有辅助信息的标记 FP 向量,因为例如 Agner Fog found 使用 addps 的输出作为 addpd 的输入时会有很大的延迟。不过,我认为两个 addpd 指令之间的 movaps,甚至 xorps 都不会导致该问题:只有实际的 FP 数学。 (FP 按位布尔运算在 Bulldozer 系列上是整数域。)


Intel 的理论吞吐量 SnB/IvB(唯一具有 AVX 而不是 AVX2 的 Intel CPU):

256b AVX 操作xorps

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

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

    因此只有 128b 或更小的 loads/stores,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 内存操作。它还将在 uop 缓存中使用 2 倍的 space,以及更多的 x86 机器代码大小。除非高速缓存库冲突,这应该 运行 吞吐量为 每 3 个时钟一个 256b-xor。


在寄存器中

在寄存器之间,每个时钟一个 256b VXORPS 和两个 128b VPXOR 会使 SnB 饱和。在 Haswell 上,每个时钟三个 AVX2 256b VPXOR 将在每个周期提供最多的 XOR-ing。 (XORPSPXOR做同样的事情,但是XORPS的输出可以转发到FP执行单元而无需额外的转发延迟周期。我猜只有一个执行单元有接线在 FP 域中得到异或结果,so Intel CPUs post-Nehalem only run XORPS on one port。)


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)。

Load/store:两个256b的加载留出两个空闲cycle来生成两个store地址,所以这样还是可以运行在每个cycle的两个loads/onestore 128b

ALU:两个端口 5 微指令 (vextractf128),两个 port0/1/5 微指令 (vpxor)。

因此,这仍然具有 每 2 个时钟一个 256b 结果的吞吐量,但它会占用更多资源并且(在 Intel 上)与 3 指令 256b 版本相比没有优势。