使用 xmm 寄存器时,AMD Jaguar/Bulldozer/Zen 上的 vxorps 归零是否比 ymm 更快?

Is vxorps-zeroing on AMD Jaguar/Bulldozer/Zen faster with xmm registers than ymm?

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

XOR-zeroing 是一种特殊情况(没有输入依赖性,on Jaguar at least avoids consuming a physical register file entry,并使来自该寄存器的 movdqa 在 issue/rename 处被消除,就像 Bulldozer 一直做的那样,即使对于非零注册)。 但是是否足够早地检测到 vxorps ymm0,ymm0,ymm0 仍然只能解码为 1 个性能与 vxorps xmm0,xmm0,xmm0 相同的宏操作? (不同于 vxorps ymm3, ymm2,ymm1

或者在已经解码为两个 uops 之后,独立检测会在以后发生吗?另外,AMD CPUs 上的矢量异或归零是否仍然使用执行端口?在 Intel-CPUs 上,Nehalem 需要一个端口,但 Sandybridge 系列在 issue/rename 阶段处理它。

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。

报告为 clang bug 32862 and gcc bug 80636。 MSVC 已经使用 xmm。尚未向 ICC 报告,ICC 也使用 zmm regs 进行 AVX512 归零。 (尽管英特尔可能不关心改变,因为目前任何英特尔 CPU 都没有好处,只有 AMD。如果他们发布低功耗 CPU 将矢量分成两半,他们可能会。他们目前的低功耗设计 (Silvermont) 根本不支持 AVX,只支持 SSE4。)


据我所知,使用 AVX-128 指令将 256b 寄存器置零的唯一可能缺点是它不会触发 Intel CPUs 上 256b 执行单元的预热。可能击败试图让它们暖起来的 C 或 C++ 黑客。

(在第一个 256b 指令之后的前 ~56k 个周期,256b 向量指令速度较慢。请参阅 Agner Fog 的 microarch pdf 中的 Skylake 部分)。如果调用 noinline 函数而 returns _mm256_setzero_ps 不是预热执行单元的可靠方法,则可能没问题。 (在没有 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。对于您调用一次以在关键循环之前预热(或保持温暖)执行单元的东西来说,这仍然是微不足道的。如果你想要内联的东西,你可能需要 inline-asm。)


我没有 AMD 硬件,所以无法测试。

如果有人拥有 AMD 硬件但不知道如何测试,请使用性能计数器来计算周期(最好是 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() 函数调用函数.

Assemble 和 nasm -felf64 vxor-zero.asm && ld -o vxor-zero vxor-zero.o 生成静态二进制文件。 (或使用 the asm-link script I posted in a Q&A about assembling static/dynamic binaries with/without libc)。

i7-6700k(英特尔 Skylake)上的输出示例,频率为 3.9GHz。 (IDK 为什么我的机器在闲置几分钟后才升至 3.9GHz。Turbo 升至 4.2 或 4.4GHz 可在启动后正常工作)。因为我使用的是性能计数器,所以机器的时钟速度实际上并不重要运行。不涉及 loads/stores 或代码缓存未命中,因此无论它们有多长,所有内容的核心时钟周期数都是恒定的。

$ 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 个)。计数几乎相同,因为没有 b运行ch 错误预测(这导致投机性发行的 uops 被丢弃而不是退役)。

uops_executed_thread 是未融合域 uops(执行端口)。 ,所以实际执行的只是 dec 和 b运行ch 微指令。 (如果我们将 ope运行ds 更改为 vxorps,那么它不仅仅是将寄存器归零,例如 vxorps ymm2, ymm1,ymm0 将输出写入下一个不读取的寄存器,执行的 uops 将匹配融合域 uop 计数。我们会看到吞吐量限制是每个时钟三个 vxorps。)

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

在循环中使用不同数量的微指令,情况就不那么好了。例如一个 5 微指令循环仅以每个时钟 3.75 微指令发出。我有意选择它为 8 微指令(当 vxorps 解码为单微指令时)。

Zen 的 issue-width 是每周期 6 微指令,所以它可能在不同的展开量下做得更好。 (有关 uop 计数不是问题宽度的倍数的短循环的更多信息,请参阅 ,在英特尔 SnB 系列 uarches 上)。

在 AMD Ryzen 上,将 ymm 寄存器与其自身进行异或运算会生成两个微操作,而将 xmm 寄存器与其自身进行异或运算只会生成一个微操作。因此,xeroing ymm 寄存器的最佳方法是将相应的 xmm 寄存器与其自身进行异或运算,并依赖于隐式零扩展。

目前唯一支持 AVX512 的处理器是 Knights Landing。它使用单个微操作对 zmm 寄存器进行异或运算。通过将向量大小一分为二来处理新的向量大小扩展是很常见的。这发生在从 64 位到 128 位的转换以及从 128 位到 256 位的转换时。未来某些处理器(来自 AMD 或 Intel 或任何其他供应商)很可能会将 512 位向量拆分为两个 256 位向量,甚至四个 128 位向量。因此,将 zmm 寄存器置零的最佳方法是将 128 位寄存器与其自身异或并依赖于零扩展。你是对的,128 位 VEX 编码指令短了一到两个字节。

大多数处理器都认为寄存器与自身的异或与寄存器的先前值无关。