两个 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 手册。请参阅 x86 wiki 以获取指向该内容和其他内容的链接。)
在 Intel 硬件上,load/store 中的任何一种都可以在没有额外延迟的情况下进入 FP 或整数域。 AMD 的行为与使用 load/store 的任何一种类似,无论数据来自哪里/来自哪里。
向量移动指令的不同风格actually matter for register<-register moves。在 Intel Nehalem 上,使用错误的 mov 指令会导致旁路延迟。在 AMD Bulldozer 系列上,移动是通过寄存器重命名而不是实际复制数据(如 Intel IvB 和更高版本)处理的,dest 寄存器继承了写入 src 寄存器的域。
我读过的现有设计在处理 movapd
与 movaps
时没有任何不同。据推测,英特尔创建 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。 (XORPS
和PXOR
做同样的事情,但是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 版本相比没有优势。
我有一个 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 手册。请参阅 x86 wiki 以获取指向该内容和其他内容的链接。)
在 Intel 硬件上,load/store 中的任何一种都可以在没有额外延迟的情况下进入 FP 或整数域。 AMD 的行为与使用 load/store 的任何一种类似,无论数据来自哪里/来自哪里。
向量移动指令的不同风格actually matter for register<-register moves。在 Intel Nehalem 上,使用错误的 mov 指令会导致旁路延迟。在 AMD Bulldozer 系列上,移动是通过寄存器重命名而不是实际复制数据(如 Intel IvB 和更高版本)处理的,dest 寄存器继承了写入 src 寄存器的域。
我读过的现有设计在处理 movapd
与 movaps
时没有任何不同。据推测,英特尔创建 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。 (XORPS
和PXOR
做同样的事情,但是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 版本相比没有优势。