AMD Jaguar/Bulldozer/Zen平台上,使用xmm寄存器比ymm寄存器更快的vxorps清零技术?

12

AMD处理器通过将256b AVX指令解码为两个128b操作来进行处理。例如,在AMD Steamroller上,vaddps ymm0, ymm1,ymm1 解码为2个宏操作,吞吐量只有 vaddps xmm0, xmm1,xmm1 的一半。

XOR清零是一个特殊情况(没有输入依赖性,并且至少在Jaguar上避免使用物理寄存器文件条目),并且使得从该寄存器到movdqa的消除在发出/重命名时被启用,就像Bulldozer总是做的那样,即使对于非零寄存器也是如此。但是,是否能够及早检测到这种情况,以便vxorps ymm0,ymm0,ymm0仍然只解码为1个宏操作,并具有与vxorps xmm0,xmm0,xmm0相同的性能?(不像vxorps ymm3, ymm2,ymm1

或者独立检测是否发生在解码成两个uops之后?此外,在AMD CPU上,向量异或零仍然使用执行端口吗?在英特尔CPU上,尼哈勒姆需要一个端口,但Sandybridge系列在发出/重命名阶段处理它。Agner Fog的指令表中没有列出这种特殊情况,他的微架构指南也没有提到uops的数量。
这意味着 vxorps xmm0,xmm0,xmm0 是实现 _mm256_setzero_ps() 更好的方法。

对于 AVX512,_mm512_setzero_ps() 也通过仅使用 VEX 编码的清零惯用语来节省一个字节,而不是 EVEX(即对于 zmm0-15。 vxorps xmm31,xmm31,xmm31 仍需要一个 EVEX)。 gcc / clang 目前使用任何他们想要的寄存器宽度的异或清零惯用语,而不总是使用 AVX-128。

Reported as clang bug 32862 and gcc bug 80636. MSVC已经使用xmm。尚未向ICC报告,ICC也使用zmm寄存器进行AVX512清零。(尽管英特尔可能不愿意更改,因为目前在任何英特尔CPU上都没有好处,只有AMD。如果他们发布一款将向量分成两半的低功耗CPU,他们可能会更改。他们当前的低功耗设计(Silvermont)根本不支持AVX,只支持SSE4。)
使用AVX-128指令清零256b寄存器的唯一可能缺点是它不会触发Intel CPU上的256b执行单元的预热。可能会打败试图预热它们的C或C++ hack。

在第一个256b指令之后的前~56k周期中,256b向量指令会变慢。请参阅Agner Fog的微架构pdf中的Skylake部分。如果调用返回_mm256_setzero_ps的noinline函数不是一种可靠的预热执行单元的方法,那么这可能没关系。一个在AVX2没有问题的、避免任何加载(可能会缓存未命中)的方法是__m128 onebits = _mm_castsi128_ps(_mm_set1_epi8(0xff)); return _mm256_insertf128_ps(_mm256_castps128_ps256(onebits), onebits),它应该编译成pcmpeqd xmm0,xmm0,xmm0/vinsertf128 ymm0,xmm0,1。这对于你调用一次来预热(或保持预热)执行单元远远超过关键循环是非常简单的。如果您想要内联的东西,您可能需要内联汇编。


我没有AMD硬件,无法测试此内容。
如果有人拥有AMD硬件但不知道如何测试,请使用perf计数器来计算周期(最好是m-ops或uops或AMD所称的其他指令)。
这是我用来测试短序列的NASM/YASM源代码:
section .text
global _start
_start:

    mov     ecx, 250000000

align 32  ; shouldn't matter, but just in case
.loop:

    dec     ecx  ; prevent macro-fusion by separating this from jnz, to avoid differences on CPUs that can't macro-fuse

%rep 6
    ;    vxorps  xmm1, xmm1, xmm1
    vxorps  ymm1, ymm1, ymm1
%endrep

    jnz .loop

    xor edi,edi
    mov eax,231    ; exit_group(0) on x86-64 Linux
    syscall

如果你不在Linux上,也许可以用ret替换循环后面的内容(退出系统调用),并从C main()函数中调用该函数。
使用nasm -felf64 vxor-zero.asm && ld -o vxor-zero vxor-zero.o进行汇编,生成静态二进制文件。(或者使用我在有关使用/不使用libc汇编静态/动态二进制文件的Q&A中发布的asm-link脚本)。
以下是在i7-6700k(Intel Skylake)上的示例输出,时钟频率为3.9GHz。(我不知道为什么我的机器在闲置几分钟后只能达到3.9GHz。在启动后立即正常工作的Turbo可达4.2或4.4GHz)。由于我正在使用perf计数器,因此机器实际运行的时钟速度并不重要。没有涉及负载/存储或代码缓存未命中,因此每个操作的核心时钟周期数都是恒定的,无论它们持续多长时间。
$ alias disas='objdump -drwC -Mintel'
$ b=vxor-zero;  asm-link "$b.asm" && disas "$b" && ocperf.py stat -etask-clock,cycles,instructions,branches,uops_issued.any,uops_retired.retire_slots,uops_executed.thread -r4 "./$b"
+ yasm -felf64 -Worphan-labels -gdwarf2 vxor-zero.asm
+ ld -o vxor-zero vxor-zero.o

vxor-zero:     file format elf64-x86-64


Disassembly of section .text:

0000000000400080 <_start>:
  400080:       b9 80 b2 e6 0e          mov    ecx,0xee6b280
  400085:       66 66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00    data16 data16 data16 data16 data16 nop WORD PTR cs:[rax+rax*1+0x0]
  400094:       66 66 66 2e 0f 1f 84 00 00 00 00 00     data16 data16 nop WORD PTR cs:[rax+rax*1+0x0]

00000000004000a0 <_start.loop>:
  4000a0:       ff c9                   dec    ecx
  4000a2:       c5 f4 57 c9             vxorps ymm1,ymm1,ymm1
  4000a6:       c5 f4 57 c9             vxorps ymm1,ymm1,ymm1
  4000aa:       c5 f4 57 c9             vxorps ymm1,ymm1,ymm1
  4000ae:       c5 f4 57 c9             vxorps ymm1,ymm1,ymm1
  4000b2:       c5 f4 57 c9             vxorps ymm1,ymm1,ymm1
  4000b6:       c5 f4 57 c9             vxorps ymm1,ymm1,ymm1
  4000ba:       75 e4                   jne    4000a0 <_start.loop>
  4000bc:       31 ff                   xor    edi,edi
  4000be:       b8 e7 00 00 00          mov    eax,0xe7
  4000c3:       0f 05                   syscall

(ocperf.py is a wrapper with symbolic names for CPU-specific events.  It prints the perf command it actually ran):

perf stat -etask-clock,cycles,instructions,branches,cpu/event=0xe,umask=0x1,name=uops_issued_any/,cpu/event=0xc2,umask=0x2,name=uops_retired_retire_slots/,cpu/event=0xb1,umask=0x1,name=uops_executed_thread/ -r4 ./vxor-zero

 Performance counter stats for './vxor-zero' (4 runs):

        128.379226      task-clock:u (msec)       #    0.999 CPUs utilized            ( +-  0.07% )
       500,072,741      cycles:u                  #    3.895 GHz                      ( +-  0.01% )
     2,000,000,046      instructions:u            #    4.00  insn per cycle           ( +-  0.00% )
       250,000,040      branches:u                # 1947.356 M/sec                    ( +-  0.00% )
     2,000,012,004      uops_issued_any:u         # 15578.938 M/sec                   ( +-  0.00% )
     2,000,008,576      uops_retired_retire_slots:u # 15578.911 M/sec                   ( +-  0.00% )
       500,009,692      uops_executed_thread:u    # 3894.787 M/sec                    ( +-  0.00% )

       0.128516502 seconds time elapsed                                          ( +-  0.09% )

“+- 0.02%”这个数字是因为我运行了perf stat -r4,所以我的二进制文件被运行了4次。

uops_issued_anyuops_retired_retire_slots属于融合域(在Skylake和Bulldozer系列上的前端吞吐量限制为每个时钟4个)。这两个计数几乎相同,因为没有分支预测错误(导致规范发出的uops被丢弃而不是退役)。

uops_executed_thread是未融合域的uops(执行端口)。xor清零在英特尔CPU上不需要任何操作, 因此实际执行的只有dec和branch uops。(如果我们将操作数更改为vxorps,因此它不仅仅是将寄存器清零,例如vxorps ymm2, ymm1,ymm0将输出写入下一个不读取的寄存器,则执行的uops将与融合域uop计数相匹配。我们会发现吞吐量限制为每个时钟三个vxorps。)

500M个时钟周期内发出2000M个融合域uops,每个时钟周期发出4.0个uops:达到理论最大前端吞吐量。6 * 250等于1500,因此这些计数与Skylake解码vxorps ymm, ymm, ymm为1个融合域uop相匹配。

在循环中uop数量不同时,情况并不理想。例如,一个包含5个uop的循环每个时钟周期只能发出3.75个uop。我故意选择了8个uop(当vxorps解码为单个uop时)。
Zen的问题宽度为每个周期6个uops,因此使用不同数量的展开可能会更好。(有关Intel SnB系列uarches上短循环其uop计数不是问题宽度倍数的更多信息,请参见this Q&A)。

“xor-zeroing” 只有在源寄存器与目的地寄存器匹配时才会被检测到,还是所有组合(例如 vxorps ymm2,ymm4,ymm4 或类似的操作)通常都会被检测到? - TLW
1
@TLW:只有匹配的两个源是检测清零习语所必需的。因此,您可以通过使用vxorps xmm15, xmm0, xmm0(2字节VEX)而不是vxorps xmm15, xmm15, xmm15(第二个源是高寄存器需要3字节VEX)来节省一个机器码大小的字节。请参见什么是在Knights Landing上清除单个或多个ZMM寄存器的最有效方法? / 在x86汇编中将寄存器设置为零的最佳方法是什么:xor、mov还是and? - Peter Cordes
1
很不可能任何CPU从3路匹配中获得任何优势,而且检查它需要更多的硬件。在寄存器重命名中,每次写操作都必须选择一个新的物理寄存器,因此如果它确定正在写入一个常量零,则会将其写入其中。或者在Sandybridge系列上,只需将寄存器重命名指向一个硬连线的零寄存器即可。所以,对目标执行的操作与它作为源的同一架构寄存器无关。 - Peter Cordes
谢谢!只是检查我的假设;我之前见过更傻的事情。(那个“vxorps”示例实际上促使了我的问题。) - TLW
还有,你有没有任何关于AVX512的问题是你不知道答案的? - TLW
1
@TLW:希望不是这样:P 我没有在每一个问题上都发表评论或回答,但至少看过它们。这是我关注的标签之一,也是我认为比较有趣的标签之一。(而且是新的,所以关于它的问题更有趣,不会是已知想法的重复或微小变化。) - Peter Cordes
1个回答

15
在AMD Ryzen上,将ymm寄存器与自身进行异或操作会生成两个微操作,而将xmm寄存器与自身进行异或操作只会生成一个微操作。因此,通过将相应的xmm寄存器与自身进行异或运算并依靠隐式零扩展来实现清零ymm寄存器是最优的方式。
目前唯一支持AVX512的处理器是Knights Landing。它使用单个微操作来对zmm寄存器进行异或操作。通常通过将向量大小的新扩展拆分为两部分来处理它。这种情况发生在从64位到128位的转换和从128位到256位的转换中。未来(来自AMD、Intel或其他供应商的)某些处理器很可能会将512位向量拆分为两个256位向量甚至四个128位向量。因此,通过将128位寄存器与自身进行异或并依靠零扩展来实现清零zmm寄存器是最优的方法。您是正确的,128位的VEX编码指令更短1或2个字节。
大多数处理器都认识到将寄存器与自身进行异或操作与寄存器的先前值无关。

5
我在谷歌计算引擎上的 Skylake-avx512 进行了 vxorps 清零测试。由于他们的 KVM 虚拟机没有 perf 计数器或 CPU 频率可访问,但从那些和其他测试中的时间结果表明,vxorps zmm 会降低最大 turbo(并可能触发 512b 执行单元的热身),而 vxorps xmmymm 则不会。如果它被解码为多个 uops,则会运行得更慢。虽然我不能发布基准测试数据,但我想说这么多。当与其他 AVX512 指令混合使用时,这当然与指令选择无关。 - Peter Cordes

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