【问题标题】:Bitwise xor of two 256-bit integers两个 256 位整数的按位异或
【发布时间】:2016-03-23 17:35:38
【问题描述】:

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

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

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

谢谢。

【问题讨论】:

  • 你尝试的时候发生了什么?
  • 我不知道。但问题是,除非我尝试了所有可能的输入,这是指数级的,否则我无法确定 __m256_load_ps 会加载与内存中完全相同的位,对吧?
  • “所有可能的输入”在此上下文中表示 2^32 位组合,这并不像在任何现代机器上看起来那么重要。当然,有一个明确的答案(现在已经给出)仍然是一个好主意,而不是仅仅依靠暴力验证。
  • @void_ptr:您只能在少数特定硬件型号上进行暴力测试。仅仅因为它可以在您的机器上运行,而没有任何文档来支持推理,就决定通常是可以的,这总是一个坏主意。例如大于 64b 的 SSE 加载/存储保证是原子的,但在许多机器上它们是原子的。在 Pentium M 上,它们被分成两个独立的 64b 操作。在multi-socket Opteron, they are extremely rarely not atomic。同样,一些指令碰巧没有修改 SnB 上的标志,但 ISA 说它们是未定义的。
  • 另外,更新了我的答案,指出如果在需要存储回内存之前,您需要将数据返回到整数寄存器中以用于其他内容,那么仅将数据移动到向量 reg 中是不值得的。

标签: sse simd avx


【解决方案1】:

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

【讨论】:

  • 哦,0.33的吞吐量比1好吗?我在想吞吐量越高越好。
  • 问题是,如果我使用 256 位指令,我需要两次加载,一次 _mm256_xor_ps,一次存储,而四次加载,两次 _mm_xor_si128 和两次存储,如果我使用 128 位指令。 _mm_xor_si128 比 _mm256_xor_ps 快的好处可能会被这些更多的加载和存储所抵消。我仍然对我原来的问题感兴趣。
  • 请注意,256b ops 在最初的几千个时钟周期之类的东西上很慢,直到 CPU 决定停止模拟它们并使执行单元的上半部分退出节能模式或其他任何情况是。有关 256b 操作的预热时间的一些讨论,请参阅 agner.org/optimize/blog/read.php?i=142#378
  • @icando:你可能是对的,但更多的指令并不一定意味着更慢 - 虽然尝试两种方式和基准测试会很有趣,但我怀疑差别不大。跨度>
  • @PaulR:看我的回答:如果需要加载/存储,那么端口 5 上的 256b XOR 单元每 2 个周期只需要一次,因此远未达到饱和。即使其中一个值已经加载到内存中,或者您不需要存储结果,您也不太可能在端口 5 上遇到瓶颈。如果很多值都存在于寄存器中,那么混合 256b xorps 和 128b pxor 可能会很好,但解包/重新打包是不值得的。无论如何,我想我现在只是在重复自己。 >.你是对的,来自 OP 实际用例的基准是决定的方式。
【解决方案2】:

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

【讨论】:

  • IIRC,英特尔 CPU 不会因使用整数加载 -> FP 指令而受到任何惩罚,反之亦然。我忘记了AMD CPU是否可以。不过,在 AMD CPU 上,xor_ps 在整数域中运行。
  • @PeterCordes,如果没有惩罚,那么整数和浮点加载指令的意义何在?那么可能有一个无类型加载指令。
  • 当他们设计它时,他们可能想到了一种延迟更少的设计的可能性。在实践中,他们最终制造了两种负载具有相同延迟的 CPU。 AMD 也是如此:根据 Agner Fog 的微架构指南,Bulldozer 系列 CPU 从加载域到 FP 域或到 ivec 域具有相同的 6c 延迟。使用何种存储指令也无关紧要。这可能只是没有为未来做好规划的情况。以及 IDK 为什么他们在 SSE2 中引入 movdqa,而不是仅仅离开 movaps
  • 实际上,在stackoverflow.com/questions/6678073/… 中,Stephen Canon 指出,与加载/存储形式相比,差异更可能体现在 reg-reg 形式中。在 SnB 之前,这是一个真正的问题,并且在 Nehalem 上使用错误的 mov xmm,xmm 指令确实会导致绕过延迟。我们只是错过了这个问题,因为我们只考虑将它们用作加载/存储。
  • @PeterCordes,很好的链接,谢谢!但是那么单双浮点加载指令又有什么意义呢?
【解决方案3】:

首先,如果您正在使用 256b 整数做其他事情(例如加/减/乘),将它们放入向量寄存器中只是为了偶尔进行异或可能不值得转移它们的开销。如果寄存器中已经有两个数字(总共使用 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 存储 -> 64b 负载适用于存储转发。 movq / pextrq 仍然很糟糕,但延迟会更低(以更多微指令为代价)。


FP 加载/存储/按位操作在架构上保证不会产生 FP 异常,即使在用于表示信号 NaN 的位模式时也是如此。只有实际的 FP 数学指令会列出数学异常:

VADDPS

SIMD 浮点异常
上溢、下溢、无效、 精度,非正规。

VMOVAPS

SIMD 浮点异常
没有。

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

在 Intel 硬件上,任一类型的加载/存储都可以转到 FP 或整数域,而无需额外延迟。无论数据去往/来自何处,AMD 的行为相似,无论使用哪种加载/存储方式。

不同风格的向量移动指令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 向量,因为例如,当使用 addps 的输出作为 addpd 的输入时,Agner Fog found 会有很大的延迟。我不认为两个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
  • 每 0.75 个周期可以发出 3 个融合域微指令,因为流水线宽度为 4 个融合域微指令。 (假设您用于 B 和 result 的寻址模式可以微融合,否则它是 5 个融合域微指令。)

  • 加载端口: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 内存操作。它还将在 uop 缓存中使用 2 倍的空间,以及更多的 x86 机器代码大小。除非缓存库发生冲突,否则这应该以每 3 个时钟一次 256b-xor 的吞吐量运行。


在寄存器中

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


Z Boson 的混合想法:

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-everything 更多的融合域微指令 (8)。

加载/存储:两次 256b 加载为生成两个存储地址留下了两个空闲周期,因此它仍然可以在每个周期以两次加载/一次 128b 存储运行。

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

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

【讨论】:

  • 很好的答案!这是一些漂亮的组装。有趣的是它看起来比内在函数更好。我有点惊讶我的混合方法比纯 SSE 版本具有更好的吞吐量。
  • 您可能需要考虑编辑您的答案以包含此答案stackoverflow.com/questions/6678073/…
  • @Zboson:是的,不错的主意。该程序集看起来非常整洁,因为我没有包含有关输入的任何内容,并且使用了 A、B 和结果的占位符,而不是关于哪个寄存器指向的位置的一些注释。 ASM 助记符比用于内在函数的超长名称更具可读性。内在名称大部分时间都很糟糕。它们更长,但仍然有您需要解码的位(例如 epu8 与 epi32)。对于它们的确切功能的完整详细信息,无论如何您都需要通过 asm mnemonic 查找它们。我会更高兴_mm_pshufb(...)
  • 也许他们想将Byte、Word、Dword、Qword 的东西保留在 C 之外,人们会因为“word " 在像 x86 这样的 32 位/64 位机器上是 16 位。
猜你喜欢
  • 1970-01-01
  • 2021-11-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-11
  • 1970-01-01
  • 2014-06-19
  • 2015-08-30
相关资源
最近更新 更多