使用xmm寄存器而不是ymm时,对于AMD Jaguar / Bulldozer / Zen的vxorps归零是否更快?

时间:2017-05-01 01:53:06

标签: assembly x86 avx micro-optimization amd-processor

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

XOR归零是一种特殊情况(没有输入依赖,on Jaguar at least avoids consuming a physical register file entry,并且可以在发出/重命名时消除来自该寄存器的movdqa,就像Bulldozer一直为非零的regs做的那样。 但是,是否及早检测到vxorps ymm0,ymm0,ymm0仍然只解码为1个宏观操作且性能与vxorps xmm0,xmm0,xmm0相同? (与vxorps ymm3, ymm2,ymm1不同)

或者,在已经解码为两个uops后,独立检测是否会发生?此外,AMD CPU上的向量xor-zeroing是否仍然使用执行端口?在Intel-CPU上,Nehalem需要一个端口,但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目前使用他们想要的任何寄存器宽度的xor-zeroing习语,而不是总是使用AVX-128。

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

我知道使用AVX-128指令清零256b寄存器唯一可能的缺点是它不会触发Intel CPU上256b执行单元的预热。可能会破坏试图加热它们的C或C ++黑客攻击。

(256b向量指令在第一个256b指令后的前~56k周期内较慢。请参阅Agner Fog的microarch pdf中的Skylake部分)。如果调用返回noinline的{​​{1}}函数并不是预热执行单元的可靠方法,那么可能没问题。 (一个在没有AVX2的情况下仍能正常工作,并且避免任何负载(可能会缓存未命中)是_mm256_setzero_ps
__m128 onebits = _mm_castsi128_ps(_mm_set1_epi8(0xff));应编译为return _mm256_insertf128_ps(_mm256_castps128_ps256(onebits), onebits) / pcmpeqd xmm0,xmm0,xmm0。对于在紧急循环之前将执行单元预热(或保持温暖)的事情,这仍然是非常微不足道的。如果你想要内联的东西,你可能需要inline-asm。)

我没有AMD硬件,所以我无法对此进行测试。

如果有人拥有AMD硬件,但不知道如何测试,请使用perf计数器计算周期(最好是m-ops或uops或AMD称之为的任何内容)。

这是我用来测试短序列的NASM / YASM源:

vinsertf128 ymm0,xmm0,1

如果您不在Linux上,可能会在循环(退出系统调用)之后用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 替换这些内容,并从C ret函数调用该函数。

main()汇总以制作静态二进制文件。 (或使用the asm-link script I posted in a Q&A about assembling static/dynamic binaries with/without libc)。

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

nasm -felf64 vxor-zero.asm && ld -o vxor-zero vxor-zero.o

+ - 0.02%的东西是因为我跑了$ 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% ) ,所以它运行我的二进制4次。

perf stat -r4uops_issued_any是融合域(Skylake和Bulldozer系列每个时钟的前端吞吐量限制为4)。计数几乎相同,因为没有分支错误预测(导致推测性发布的uop被丢弃而不是退休)。

uops_retired_retire_slots是未融合的域uops(执行端口)。 xor-zeroing doesn't need any on Intel CPUs,所以它只是实际执行的dec和branch uops。 (如果我们将操作数更改为vxorps,那么它不会将寄存器归零,例如uops_executed_thread将输出写入下一个不读取的寄存器,uops执行将匹配融合-domain uop count。我们发现吞吐量限制是每个时钟三个vxorps。)

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

在循环中使用不同数量的uops,事情并不好。例如一个5 uop循环,每个时钟仅发出3.75 uop。我故意选择这个为8 uops(当vxorps解码为单uop时)。

Zen的问题宽度是每个循环6个uop,所以在不同的展开量下它可能会做得更好。 (有关短路的详细信息,请参阅this Q&A,其中uop计数不是问题宽度的倍数,在Intel SnB系列搜索中)。

1 个答案:

答案 0 :(得分:13)

xor'ing ymm寄存器本身在AMD Ryzen上产生两个微操作,而xor'ing xmm寄存器本身只产生一个微操作。因此,对ymm寄存器进行xeroing的最佳方法是将xmm相应的xmm寄存器与其自身相关联,并依赖于隐式零扩展。

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

大多数处理器将寄存器的xor识别为独立于寄存器的先前值。