在解码器和 uop-cache 中,寻址模式不影响微融合(除了具有立即操作数的指令不能微融合 RIP 相对寻址模式)。
但是 uop 和寻址模式的某些组合不能在 ROB 中保持微融合(在无序内核中),因此英特尔 SnB 系列 CPU 在必要时会在问题之前的某个时间“取消层压”/重命名阶段。对于问题吞吐量和无序窗口大小(ROB 大小),未分层后的融合域 uop 计数是最重要的。
Intel 的优化手册在第 2.5.2.4 节:Micro-op Queue and the Loop Stream Detector (LSD)中描述了Sandybridge 的 un-lamination ,但没有描述任何后续微架构的更改。
更新:现在英特尔手册有一个详细的部分来描述 Haswell 的非层压。请参阅第 2.4.5 节去层压。SandyBridge 的简要说明在第 2.5.2.4 节中。
规则,我从 SnB、HSW 和 SKL 的实验中可以看出:
- SnB(我也假设是 IvB):索引寻址模式始终是非分层的,其他的则保持微融合。IACA(大部分?)是正确的。
- HSW、SKL:只有当索引 ALU 指令有 2 个操作数并将 dst 寄存器视为读-修改-写时,它才会保持微融合。这里的“操作数”包括标志,意思是
adc
不要cmov
微融合。大多数 VEX 编码指令也不会融合,因为它们通常具有三个操作数(因此paddb xmm0, [rdi+rbx]
会融合但vpaddb xmm0, xmm0, [rdi+rbx]
不会融合)。最后,第一个操作数是只写的偶尔的 2 操作数指令,例如pabsb xmm0, [rax + rbx]
也不熔断。IACA 是错误的,应用了 SnB 规则。
相关:简单(非索引)寻址模式是端口7(Haswell 及更高版本)上的专用存储地址单元可以处理的唯一寻址模式,因此避免存储的索引寻址模式仍然可能有用。(一个很好的技巧是用一个寄存器来寻址你的 dst,但是用 src 来寻址dst+(initial_src-initial_dst)
。然后你只需要在循环内增加 dst 寄存器。)
请注意,某些指令根本不会微融合(即使在解码器/uop-cache 中)。例如shufps xmm, [mem], imm8
,或vinsertf128 ymm, ymm, [mem], imm8
,通过 Skylake 在 SnB 上始终为 2 微指令,即使它们的寄存器源版本仅为 1 微指令。这对于具有 imm8 控制操作数加上通常的 dest/src1、src2 寄存器/内存操作数的指令来说是典型的,但还有一些其他情况。例如PSRLW/D/Q xmm,[mem]
(来自内存操作数的向量移位计数)不会进行微融合,PMULLD 也不会。
另请参阅Agner Fog 博客上的这篇文章,讨论有关读取大量寄存器时 HSW/SKL 问题吞吐量限制的讨论:与索引寻址模式的大量微融合相比,使用较少寄存器操作数的相同指令会导致速度减慢:一个-寄存器寻址模式和立即数。 我们还不知道原因,但我怀疑某种寄存器读取限制,可能与从 PRF 读取大量冷寄存器有关。
测试用例,来自实际测量的数字:这些都是解码器中的微熔丝,AFAIK,即使它们后来没有层压。
# store
mov [rax], edi SnB/HSW/SKL: 1 fused-domain, 2 unfused. The store-address uop can run on port7.
mov [rax+rsi], edi SnB: unlaminated. HSW/SKL: stays micro-fused. (The store-address can't use port7, though).
mov [buf +rax*4], edi SnB: unlaminated. HSW/SKL: stays micro-fused.
# normal ALU stuff
add edx, [rsp+rsi] SnB: unlaminated. HSW/SKL: stays micro-fused.
# I assume the majority of traditional/normal ALU insns are like add
HSW/SKL 可能必须解压的三输入指令
vfmadd213ps xmm0,xmm0,[rel buf] HSW/SKL: stays micro-fused: 1 fused, 2 unfused.
vfmadd213ps xmm0,xmm0,[rdi] HSW/SKL: stays micro-fused
vfmadd213ps xmm0,xmm0,[0+rdi*4] HSW/SKL: un-laminated: 2 uops in fused & unfused-domains.
(So indexed addressing mode is still the condition for HSW/SKL, same as documented by Intel for SnB)
# no idea why this one-source BMI2 instruction is unlaminated
# It's different from ADD in that its destination is write-only (and it uses a VEX encoding)
blsi edi, [rdi] HSW/SKL: 1 fused-domain, 2 unfused.
blsi edi, [rdi+rsi] HSW/SKL: 2 fused & unfused-domain.
adc eax, [rdi] same as cmov r, [rdi]
cmove ebx, [rdi] Stays micro-fused. (SnB?)/HSW: 2 fused-domain, 3 unfused domain.
SKL: 1 fused-domain, 2 unfused.
# I haven't confirmed that this micro-fuses in the decoders, but I'm assuming it does since a one-register addressing mode does.
adc eax, [rdi+rsi] same as cmov r, [rdi+rsi]
cmove ebx, [rdi+rax] SnB: untested, probably 3 fused&unfused-domain.
HSW: un-laminated to 3 fused&unfused-domain.
SKL: un-laminated to 2 fused&unfused-domain.
我假设 Broadwell 在 adc/cmov 中的行为类似于 Skylake。
奇怪的是,HSW 不分层内存源 ADC 和 CMOV。也许英特尔在交付 Haswell 的最后期限之前没有考虑将其从 SnB 更改。
Agner 的 insn 表说cmovcc r,m
根本adc r,m
不要在 HSW/SKL 上进行微融合,但这与我的实验不符。我测量的周期计数与融合域 uop 问题计数相匹配,对于 4 uop / 时钟问题瓶颈。希望他会仔细检查并更正表格。
内存目标整数 ALU:
add [rdi], eax SnB: untested (Agner says 2 fused-domain, 4 unfused-domain (load + ALU + store-address + store-data)
HSW/SKL: 2 fused-domain, 4 unfused.
add [rdi+rsi], eax SnB: untested, probably 4 fused & unfused-domain
HSW/SKL: 3 fused-domain, 4 unfused. (I don't know which uop stays fused).
HSW: About 0.95 cycles extra store-forwarding latency vs. [rdi] for the same address used repeatedly. (6.98c per iter, up from 6.04c for [rdi])
SKL: 0.02c extra latency (5.45c per iter, up from 5.43c for [rdi]), again in a tiny loop with dec ecx/jnz
adc [rdi], eax SnB: untested
HSW: 4 fused-domain, 6 unfused-domain. (same-address throughput 7.23c with dec, 7.19c with sub ecx,1)
SKL: 4 fused-domain, 6 unfused-domain. (same-address throughput ~5.25c with dec, 5.28c with sub)
adc [rdi+rsi], eax SnB: untested
HSW: 5 fused-domain, 6 unfused-domain. (same-address throughput = 7.03c)
SKL: 5 fused-domain, 6 unfused-domain. (same-address throughput = ~5.4c with sub ecx,1 for the loop branch, or 5.23c with dec ecx for the loop branch.)
是的,没错,//adc [rdi],eax
运行速度比使用 SKL 的相同循环快。我没有尝试使用不同的地址,因为显然 SKL 不喜欢重复重写同一地址(存储转发延迟高于预期。另请参阅这篇文章,关于重复存储/重新加载到同一地址比 SKL 上的预期慢.dec ecx
jnz
add
adc
内存目标adc
是如此之多的微指令,因为英特尔 P6 系列(显然是 SnB 系列)无法为多微指令指令的所有微指令保留相同的 TLB 条目,因此它需要一个额外的微指令来解决这个问题 -加载和添加完成的情况,然后存储出现故障,但由于 CF 已经更新,因此无法重新启动 insn。来自 Andy Glew (@krazyglew) 的一系列有趣评论。
据推测,解码器中的融合和之后的非分层使我们无需微码 ROM来从adc [base+idx], reg
.
为什么 SnB 系列不层压:
Sandybridge 简化了内部 uop 格式以节省功耗和晶体管(同时对使用物理寄存器文件进行了重大更改,而不是将输入/输出数据保存在 ROB 中)。SnB 系列 CPU 仅允许有限数量的输入寄存器用于无序内核中的融合域 uop。对于 SnB/IvB,该限制为 2 个输入(包括标志)。对于 HSW 及更高版本,uop 的限制为 3 个输入。我不确定 memory-destination是否充分利用了这一点add
,adc
或者英特尔是否必须通过一些说明让 Haswell 出局
Nehalem 和更早的版本对非融合域微指令有 2 个输入的限制,但 ROB 显然可以使用 3 个输入寄存器(非内存寄存器操作数、基址和索引)跟踪微融合微指令。
所以索引存储和 ALU+load 指令仍然可以高效解码(不必是组中的第一个 uop),并且不会在 uop 缓存中占用额外的空间,但除此之外,微融合的优势基本上没有了调优紧密的循环。 “un-lamination”发生在 4-fused-domain-uops-per-cycle issue/retire width out-of-order core 之前。融合域性能计数器 (uops_issued / uops_retired.retire_slots) 计算未分层后的融合域 uops。
英特尔对重命名器的描述(第 2.3.3.1 节:重命名器)暗示实际上是问题/重命名阶段进行了 un-lamination,因此用于 un-lamination 的 uops 仍可能在 28/56/64 fused 中进行微融合-domain uop 问题队列/循环缓冲区(又名 IDQ)。
TODO:测试一下。制作一个几乎不适合循环缓冲区的循环。改变一些东西,使其中一个微指令在发布之前不分层,并查看它是否仍然从循环缓冲区 (LSD) 运行,或者现在是否所有微指令都从微指令缓存 (DSB) 中重新获取。有性能计数器来跟踪 uops 的来源,所以这应该很容易。
更难的 TODO:如果在从 uop 缓存读取和添加到 IDQ 之间发生非分层,测试它是否可以减少 uop 缓存带宽。或者,如果在发行阶段发生未分层,是否会影响发行吞吐量?(即在发出前 4 个之后,它如何处理剩余的 uops。)
(有关基于调整一些 LUT 代码的一些猜测,请参阅此答案的先前版本,其中一些注释vpgatherdd
表明周期比pinsrw
循环多 1.7 倍。)
SnB 的实验测试
HSW/SKL 数值是在 i5-4210U 和 i7-6700k 上测量的。两者都启用了 HT(但系统空闲,因此线程拥有整个核心)。我在两个系统上运行相同的静态二进制文件,SKL 上的 Linux 4.10 和 HSW 上的 Linux 4.8,使用ocperf.py
. (HSW 笔记本 NFS 安装了我的 SKL 桌面的 /home。)
如下所述在不再工作的 i5-2500k 上测量 SnB 数。
通过对微指令和周期的性能计数器进行测试来确认。
我找到了 Intel Sandybridge 的 PMU 事件表,用于 Linux 的perf
命令。(perf
不幸的是,对于大多数特定于硬件的 PMU 事件,标准没有符号名称,例如 uops。)我在最近的回答中使用了它。
ocperf.py
为这些特定于 uarch 的 PMU 事件提供符号名称,因此您不必查找表。此外,相同的符号名称适用于多个 uarches。当我第一次写这个答案时,我并没有意识到这一点。
为了测试 uop 微融合,我构建了一个测试程序,该程序在 Intel CPU 的 4-uops-per-cycle fused-domain 限制上遇到了瓶颈。为了避免任何执行端口争用,这些 uop 中的许多都是nop
s,它们仍然位于 uop 缓存中,并与任何其他 uop 一样通过管道,除了它们不会被分派到执行端口。(一个xor x, same
或一个被淘汰的动作是一样的。)
测试程序:yasm -f elf64 uop-test.s && ld uop-test.o -o uop-test
GLOBAL _start
_start:
xor eax, eax
xor ebx, ebx
xor edx, edx
xor edi, edi
lea rsi, [rel mydata] ; load pointer
mov ecx, 10000000
cmp dword [rsp], 2 ; argc >= 2
jge .loop_2reg
ALIGN 32
.loop_1reg:
or eax, [rsi + 0]
or ebx, [rsi + 4]
dec ecx
nop
nop
nop
nop
jg .loop_1reg
; xchg r8, r9 ; no effect on flags; decided to use NOPs instead
jmp .out
ALIGN 32
.loop_2reg:
or eax, [rsi + 0 + rdi]
or ebx, [rsi + 4 + rdi]
dec ecx
nop
nop
nop
nop
jg .loop_2reg
.out:
xor edi, edi
mov eax, 231 ; exit(0)
syscall
SECTION .rodata
mydata:
db 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
我还发现,如果循环不是 4 uop 的倍数,则循环缓冲区外的 uop 带宽不是恒定的 4 每个周期。(即它是abc
, abc
, ...;不是abca
, bcab
, ...)。不幸的是,Agner Fog 的 microarch 文档并不清楚循环缓冲区的这种限制。请参阅执行 uop 计数不是处理器宽度倍数的循环时性能会降低吗?有关 HSW/SKL 的更多调查。在这种情况下,SnB 可能比 HSW 更差,但我不确定并且仍然没有可用的 SnB 硬件。
我想把宏融合(比较和分支)排除在外,所以我在分支和分支nop
之间使用了 s dec
。我使用了 4nop
秒,因此使用微融合,循环将是 8 微秒,并且每 1 次迭代以 2 个周期填充管道。
在循环的另一个版本中,使用不进行微融合的 2 操作数寻址模式,循环将是 10 个融合域微指令,并在 3 个周期内运行。
我的 3.3GHz 英特尔 Sandybridge (i5 2500k) 的结果。 我没有做任何事情来让 cpufreq 调节器在测试之前提高时钟速度,因为周期是你不与内存交互时的周期。我为必须以十六进制输入的性能计数器事件添加了注释。
测试 1-reg 寻址模式:无 cmdline arg
$ perf stat -e task-clock,cycles,instructions,r1b1,r10e,r2c2,r1c2,stalled-cycles-frontend,stalled-cycles-backend ./uop-test
Performance counter stats for './uop-test':
11.489620 task-clock (msec) # 0.961 CPUs utilized
20,288,530 cycles # 1.766 GHz
80,082,993 instructions # 3.95 insns per cycle
# 0.00 stalled cycles per insn
60,190,182 r1b1 ; UOPS_DISPATCHED: (unfused-domain. 1->umask 02 -> uops sent to execution ports from this thread)
80,203,853 r10e ; UOPS_ISSUED: fused-domain
80,118,315 r2c2 ; UOPS_RETIRED: retirement slots used (fused-domain)
100,136,097 r1c2 ; UOPS_RETIRED: ALL (unfused-domain)
220,440 stalled-cycles-frontend # 1.09% frontend cycles idle
193,887 stalled-cycles-backend # 0.96% backend cycles idle
0.011949917 seconds time elapsed
测试 2-reg 寻址模式:使用 cmdline arg
$ perf stat -e task-clock,cycles,instructions,r1b1,r10e,r2c2,r1c2,stalled-cycles-frontend,stalled-cycles-backend ./uop-test x
Performance counter stats for './uop-test x':
18.756134 task-clock (msec) # 0.981 CPUs utilized
30,377,306 cycles # 1.620 GHz
80,105,553 instructions # 2.64 insns per cycle
# 0.01 stalled cycles per insn
60,218,693 r1b1 ; UOPS_DISPATCHED: (unfused-domain. 1->umask 02 -> uops sent to execution ports from this thread)
100,224,654 r10e ; UOPS_ISSUED: fused-domain
100,148,591 r2c2 ; UOPS_RETIRED: retirement slots used (fused-domain)
100,172,151 r1c2 ; UOPS_RETIRED: ALL (unfused-domain)
307,712 stalled-cycles-frontend # 1.01% frontend cycles idle
1,100,168 stalled-cycles-backend # 3.62% backend cycles idle
0.019114911 seconds time elapsed
因此,两个版本都运行了 80M 指令,并向执行端口发送了 60M 微指令。(or
内存源为 分配到 ALU,为加载分配or
一个加载端口,无论它是否在管道的其余部分中进行了微融合。 nop
根本不分派到执行端口。)类似地,两个版本都淘汰了 100M 的未融合域 uops,因为这里计算了 40M 的 nops。
不同之处在于融合域的计数器。
- 1 寄存器地址版本仅发布和淘汰 80M 融合域微指令。这与指令的数量相同。每个insn变成一个融合域uop。
- 2 寄存器地址版本发出 100M 融合域微指令。这与未融合域 uops 的数量相同,表明没有发生微融合。
我怀疑如果分支错误预测导致 uops 在发布后但在退休之前被取消,您只会看到 UOPS_ISSUED 和 UOPS_RETIRED(已使用退休槽)之间的差异。
最后,性能影响是真实存在的。 非融合版本需要 1.5 倍的时钟周期。与大多数实际情况相比,这夸大了性能差异。循环必须在整个周期内运行(在 LSD 不太复杂的 Sandybridge 上),并且 2 个额外的微指令将其从 2 推到 3。通常,额外的 2 个融合域微指令会产生较小的差异。如果代码被 4-fused-domain-uops-per-cycle 以外的东西阻塞,则可能没有区别。
[base + immediate offset]
尽管如此,如果使用适度的展开和递增多个指针(这些指针用于简单寻址,而不是使用[base + index]
寻址模式)来实现,那么在循环中进行大量内存引用的代码可能会更快。
进一步的东西
RIP-relative with a immediate can't micro-fuse。Agner Fog 的测试表明,即使在解码器/uop-cache 中也是如此,因此它们从一开始就不会融合(而不是未分层)。
IACA 弄错了,并声称这两个微型保险丝:
cmp dword [abs mydata], 0x1b ; fused counters != unfused counters (micro-fusion happened, and wasn't un-laminated). Uses 2 entries in the uop-cache, according to Agner Fog's testing
cmp dword [rel mydata], 0x1b ; fused counters ~= unfused counters (micro-fusion didn't happen)
cmp
(对于/ ,微+宏融合都有更多限制jcc
。TODO:写下来以测试内存位置。)
当没有立即数时,RIP-rel 会进行微融合(并保持融合),例如:
or eax, dword [rel mydata] ; fused counters != unfused counters, i.e. micro-fusion happens
微融合不会增加指令的延迟。负载可以在其他输入准备好之前发出。
ALIGN 32
.dep_fuse:
or eax, [rsi + 0]
or eax, [rsi + 0]
or eax, [rsi + 0]
or eax, [rsi + 0]
or eax, [rsi + 0]
dec ecx
jg .dep_fuse
eax
由于dep 链,此循环每次迭代运行 5 个周期。or eax, [rsi + 0 + rdi]
不比, 或的序列快mov ebx, [rsi + 0 + rdi] / or eax, ebx
。(未融合的版本和mov
版本都运行相同数量的 uops。)调度/dep 检查发生在未融合的域中。新发布的微指令进入调度程序(又名预订站 (RS))以及 ROB。它们在调度后离开调度程序(也就是被发送到执行单元),但留在 ROB 中直到退休。因此,隐藏加载延迟的无序窗口至少是调度程序的大小(Sandybridge 中的 54 个未融合域微指令,Haswell 中的 60 个,Skylake 中的 97 个)。
微融合没有将基数和偏移量作为同一个寄存器的快捷方式。带有 or eax, [mydata + rdi+4*rdi]
(其中 rdi 为零)的循环运行与带有 的循环一样多的微指令和循环or eax, [rsi+rdi]
。这种寻址模式可用于迭代从固定地址开始的奇数大小的结构数组。这可能从未在大多数程序中使用过,因此英特尔没有花费晶体管来允许这种特殊情况下的 2 寄存器模式进行微熔断也就不足为奇了。(无论如何,英特尔将其记录为“索引寻址模式”,其中需要寄存器和比例因子。)
cmp
/jcc
或dec
/的宏融合创建了jcc
一个即使在未融合域中也保持为单个 uop 的 uop。 dec / nop / jge
仍然可以在一个周期中运行,但是是三个微指令而不是一个。