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
)
这意味着
vxorps xmm0,xmm0,xmm0
是实现 _mm256_setzero_ps()
更好的方法。对于 AVX512,_mm512_setzero_ps()
也通过仅使用 VEX 编码的清零惯用语来节省一个字节,而不是 EVEX(即对于 zmm0-15。 vxorps xmm31,xmm31,xmm31
仍需要一个 EVEX)。 gcc / clang 目前使用任何他们想要的寄存器宽度的异或清零惯用语,而不总是使用 AVX-128。
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_any
和uops_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相匹配。
Zen的问题宽度为每个周期6个uops,因此使用不同数量的展开可能会更好。(有关Intel SnB系列uarches上短循环其uop计数不是问题宽度倍数的更多信息,请参见this Q&A)。
vxorps ymm2,ymm4,ymm4
或类似的操作)通常都会被检测到? - TLWvxorps xmm15, xmm0, xmm0
(2字节VEX)而不是vxorps xmm15, xmm15, xmm15
(第二个源是高寄存器需要3字节VEX)来节省一个机器码大小的字节。请参见什么是在Knights Landing上清除单个或多个ZMM寄存器的最有效方法? / 在x86汇编中将寄存器设置为零的最佳方法是什么:xor、mov还是and?。 - Peter Cordes