[英]Micro fusion and addressing modes
我使用英特尔® 架构代码分析器(IACA) 发现了一些意外(对我而言)。
以下指令使用[base+index]
寻址
addps xmm1, xmmword ptr [rsi+rax*1]
根据 IACA,它没有微型保险丝。 但是,如果我像这样使用[base+offset]
addps xmm1, xmmword ptr [rsi]
IACA 报告说它确实熔断了。
Intel 优化参考手册的第 2-11 节给出了以下作为“所有解码器都可以处理的微融合微操作”的示例
FADD DOUBLE PTR [RDI + RSI*8]
和Agner Fog 的优化汇编手册也给出了使用[base+index]
寻址的微操作融合的例子。 例如,参见第 12.2 节“Core2 上的相同示例”。 那么正确答案是什么呢?
在解码器和 uop-cache 中,寻址模式不影响微融合(除了带有立即操作数的指令不能微融合 RIP 相对寻址模式)。
但是某些 uop 和寻址模式的组合无法在 ROB 中保持微融合(在乱序内核中),因此英特尔 SnB 系列 CPU 在必要时会在问题发生之前的某个时间点“取消层压”/重命名阶段。 对于问题吞吐量和乱序窗口大小(ROB 大小),取消层压后的融合域 uop 计数很重要。
Intel 的优化手册在第 2.5.2.4 节:微操作队列和循环流检测器 (LSD)中描述了 Sandybridge 的取消层压,但没有描述任何后续微架构的更改。
更新:现在英特尔手册有一个详细的部分来描述 Haswell 的取消层压。 参见章节 2.4.5 拆层。 SandyBridge 的简要说明在第 2.5.2.4 节。
这些规则,我从 SnB、HSW 和 SKL 的实验中可以看出:
adc
和cmov
不会微熔断器。 大多数 VEX 编码指令也不融合,因为它们通常具有三个操作数(因此paddb xmm0, [rdi+rbx]
融合但vpaddb xmm0, xmm0, [rdi+rbx]
没有)。 最后,偶尔第一个操作数为只写的2-操作数指令,如pabsb xmm0, [rax + rbx]
也不熔断。 IACA 是错误的,应用了 SnB 规则。 相关:简单(非索引)寻址模式是端口 7(Haswell 及更高版本)上的专用存储地址单元可以处理的唯一模式,因此避免存储的索引寻址模式仍然可能有用。 (对此的一个好技巧是使用单个寄存器处理您的 dst,但使用dst+(initial_src-initial_dst)
src。然后您只需在循环内增加 dst 寄存器。)
请注意,某些指令根本不会进行微熔断器(即使在解码器/uop 缓存中)。 例如shufps xmm, [mem], imm8
或vinsertf128 ymm, ymm, [mem], imm8
在 SnB 上通过 Skylake 总是 2 uop,即使它们的寄存器源版本只有 1 uop。 这对于具有 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
/ dec ecx
/ jnz
比在 SKL 上使用add
而不是adc
的相同循环运行得更快。 我没有尝试使用不同的地址,因为显然 SKL 不喜欢重复重写同一地址(存储转发延迟高于预期。另见这篇关于重复存储/重新加载到相同地址的文章比 SKL 上的预期慢.
内存目标adc
有很多 uop,因为 Intel P6 系列(显然是 SnB 系列)无法为多 uop 指令的所有 uops 保留相同的 TLB 条目,因此需要额外的 uop 来解决该问题- 在加载和添加完成,然后存储出现故障的情况下,但不能重新启动 insn,因为 CF 已经更新。 来自 Andy Glew (@krazyglew) 的一系列有趣评论。
据推测,解码器中的融合和稍后的取消层压使我们无需使用微码 ROM从adc [base+idx], reg
的单个指令生成超过 4 个融合域 uops。
为什么 SnB 系列不分层:
Sandybridge 简化了内部 uop 格式以节省功率和晶体管(同时对使用物理寄存器文件进行了重大更改,而不是将输入/输出数据保存在 ROB 中)。 SnB 系列 CPU 在乱序内核中只允许有限数量的输入寄存器用于融合域 uop。 对于 SnB/IvB,该限制为 2 个输入(包括标志)。 对于 HSW 及更高版本,一个 uop 的限制为 3 个输入。 我不确定 memory-destination add
和adc
是否充分利用了这一点,或者英特尔是否必须通过一些说明让 Haswell 走出门外
Nehalem 及更早版本的未融合域 uop 限制为 2 个输入,但 ROB 显然可以使用 3 个输入寄存器(非内存寄存器操作数、基址和索引)跟踪微融合 uops。
所以索引存储和 ALU+加载指令仍然可以高效解码(不必是组中的第一个 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 节:重命名器)暗示它是问题/重命名阶段实际执行取消层压,因此用于取消层压的 uops 可能仍会在 28/56/64 融合中微融合-domain uop 问题队列/循环缓冲区(又名 IDQ)。
TODO:测试一下。 制作一个几乎适合循环缓冲区的循环。 更改某些内容,以便其中一个 uops 将在发布前取消层压,并查看它是否仍从循环缓冲区 (LSD) 运行,或者所有 uops 现在是否都从 uop 缓存 (DSB) 中重新获取。 有 perf 计数器来跟踪 uops 的来源,所以这应该很容易。
更难的 TODO:如果在从 uop 缓存读取和添加到 IDQ 之间发生未分层,请测试它是否可以减少 uop 缓存带宽。 或者,如果在发行阶段发生未层压,是否会影响发行吞吐量? (即在发出第一个 4 之后它如何处理剩余的 uops。)
(有关基于调整某些 LUT 代码的一些猜测,请参阅此答案的先前版本,其中关于vpgatherdd
一些注释比pinsrw
循环多出pinsrw
周期。)
HSW/SKL 数值是在 i5-4210U 和 i7-6700k 上测量的。 两者都启用了 HT(但系统空闲,因此线程拥有整个内核)。 我使用ocperf.py
在两个系统上运行相同的静态二进制文件,SKL 上的 Linux 4.10 和 HSW 上的 Linux 4.8。 (HSW 笔记本电脑 NFS 安装在我的 SKL 桌面的 /home 上。)
SnB 数量的测量如下所述,在不再工作的 i5-2500k 上。
通过对 uops 和周期的性能计数器进行测试来确认。
我找到了一张英特尔 Sandybridge 的 PMU 事件表,用于 Linux 的perf
命令。 (遗憾的是,标准perf
没有大多数特定于硬件的 PMU 事件的符号名称,例如 uops。)我在最近的答案中使用了它。
ocperf.py
为这些特定于 uarch 的 PMU 事件提供符号名称,因此您不必查找表。 此外,相同的符号名称适用于多个 uarch。 当我第一次写这个答案时,我并不知道。
为了测试 uop 微融合,我构建了一个测试程序,该程序在 Intel CPU 的每周期 4 uop 融合域限制上存在瓶颈。 为了避免任何执行端口争用,这些 uop 中的许多都是nop
,它们仍然位于 uop 缓存中,并与任何其他 uop 一样通过管道,只是它们不会被分派到执行端口。 ( xor x, same
或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 的微架构文档并不清楚循环缓冲区的这种限制。 请参阅执行 uop 计数不是处理器宽度倍数的循环时性能是否会降低? 有关 HSW/SKL 的更多调查。 在这种情况下,SnB 可能比 HSW 更糟糕,但我不确定,也没有可用的 SnB 硬件。
我想将宏融合(比较和分支)排除在图片之外,所以我在dec
和分支之间使用了nop
。 我使用了 4 个nop
,因此使用微融合,循环将是 8 个 uops,并且每 1 次迭代以 2 个周期填充管道。
在另一个版本的循环中,使用不使用微融合的 2 操作数寻址模式,循环将是 10 个融合域 uops,运行 3 个周期。
来自我的 3.3GHz Intel 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 uop 分派到执行端口。 ( or
使用内存源为or
分派到 ALU,并为负载分配一个加载端口,无论它是否在管道的其余部分进行微融合nop
根本不分派到执行端口.) 同样,两个版本都停用了 100M 未融合域 uops,因为 40M nops 在这里也算在内。
不同之处在于融合域的计数器。
我怀疑如果分支错误预测导致 uops 在发布后被取消,但在退休之前,您只会看到 UOPS_ISSUED 和 UOPS_RETIRED(已使用退休槽)之间的区别。
最后,性能影响是真实的。 非融合版本需要 1.5 倍的时钟周期。 与大多数实际情况相比,这夸大了性能差异。 循环必须在整数个周期内运行( 在 LSD 不太复杂的 Sandybridge 上),并且 2 个额外的 uops 将它从 2 推到 3。通常,额外的 2 个融合域 uops 会产生较小的差异。 如果代码受到 4-fused-domain-uops-per-cycle 以外的其他东西的阻碍,则可能没有区别。
尽管如此,在循环中进行大量内存引用的代码可能会更快,如果使用适度的展开和递增多个指针来实现,这些指针与简单的[base + immediate offset]
寻址一起使用,而不是使用[base + index]
寻址模式。
RIP-relative 与直接不能 micro-fuse 。 Agner Fog 的测试表明,即使在解码器 / uop 缓存中也是如此,因此它们从一开始就不会融合(而不是未层压)。
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
,微+宏融合还有更多限制jcc
:写下它以测试内存位置。)
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
的序列快。 (unfused 和mov
版本都运行相同数量的 uops。)调度/dep 检查发生在 unfused 域中。 新发布的 uops 进入调度程序(又名预订站 (RS))和 ROB。 他们在分派后离开调度程序(也就是被发送到执行单元),但留在 ROB 中直到退休。 因此,用于隐藏负载延迟的无序窗口至少是调度程序大小( Sandybridge 中为 54 个未融合域 uops,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
仍然可以在一个循环中运行,但它是三个 uops 而不是一个。
注意:自从我写了这个答案,彼得也测试了 Haswell 和 Skylake,并将结果整合到上面接受的答案中(特别是,我认为下面的 Skylake 的大部分改进似乎实际上已经出现在 Haswell 中)。 您应该看到有关跨 CPU 的行为纲要的答案,并且这个答案(尽管不是错误的)主要具有历史意义。
我的测试表明,与 Sandybridge 不同,在Skylake至少1 上,处理器甚至完全融合了复杂的寻址模式。
也就是说,上面由 Peter 发布的代码的1-arg和2-arg版本在相同数量的循环中运行,具有相同数量的 uops 调度和退出。
我的结果:
./uop-test
性能计数器统计信息:
23.718772 task-clock (msec) # 0.973 CPUs utilized
20,642,233 cycles # 0.870 GHz
80,111,957 instructions # 3.88 insns per cycle
60,253,831 uops_executed_thread # 2540.344 M/sec
80,295,685 uops_issued_any # 3385.322 M/sec
80,176,940 uops_retired_retire_slots # 3380.316 M/sec
0.024376698 seconds time elapsed
./uop-test x
性能计数器统计信息:
13.532440 task-clock (msec) # 0.967 CPUs utilized
21,592,044 cycles # 1.596 GHz
80,073,676 instructions # 3.71 insns per cycle
60,144,749 uops_executed_thread # 4444.487 M/sec
80,162,360 uops_issued_any # 5923.718 M/sec
80,104,978 uops_retired_retire_slots # 5919.478 M/sec
0.013997088 seconds time elapsed
./uop-test xx
性能计数器统计信息:
16.672198 task-clock (msec) # 0.981 CPUs utilized
27,056,453 cycles # 1.623 GHz
80,083,140 instructions # 2.96 insns per cycle
60,164,049 uops_executed_thread # 3608.645 M/sec
100,187,390 uops_issued_any # 6009.249 M/sec
100,118,409 uops_retired_retire_slots # 6005.112 M/sec
0.016997874 seconds time elapsed
我没有在 Skylake 上找到任何 UOPS_RETIRED_ANY 指令,只有显然是融合域的“退休插槽”家伙。
最后的测试 ( uop-test xx
) 是 Peter 建议的一个变体,它使用 RIP 相关的cmp
和即时,已知不会微熔:
.loop_riprel
cmp dword [rel mydata], 1
cmp dword [rel mydata], 2
dec ecx
nop
nop
nop
nop
jg .loop_riprel
结果表明,每个周期额外的 2 个 uops 被发出和退出的 uops 计数器接收(因此该测试可以区分融合发生与否)。
欢迎对其他架构进行更多测试! 您可以在 github 中找到代码(从上面的 Peter 复制)。
[1] ...也许还有一些介于 Skylake 和 Sandybridge 之间的其他架构,因为 Peter 只测试了 SB,而我只测试了 SKL。
没有 uop 缓存的较旧的 Intel 处理器可以进行融合,所以这可能是 uop 缓存的一个缺点。 我现在没有时间对此进行测试,但是下次更新我的测试脚本时,我会为 uop fusion 添加一个测试。 您是否尝试过 FMA 指令? 它们是唯一允许在未融合 uop 中包含 3 个输入依赖项的指令。
我现在已经查看了 Intel Sandy Bridge、Ivy Bridge、Haswell 和 Broadwell 的测试结果。 我还没有机会在 Skylake 上进行测试。 结果是:
您的结果可能是由于其他因素造成的。 我没有尝试使用 IACA。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.