32位与64位寄存器是否会造成CPU微架构层面的性能差异?
我正在对比Peter Cordes在「将CPU寄存器所有位设置为1」相关问题回答中提到的实现方法,为此编写了基准测试程序,将除e/rsp、e/rbp、e/rcx外的13个寄存器全部置为全1。
测试代码如下,times 32 nop用于避免DSB和LSD的影响:
mov ecx, 100000000 Align 32 .test3: times 32 nop mov rax,-1 mov rbx,-1 ;mov ecx,-1 mov rdx,-1 mov rdi,-1 mov rsi,-1 mov r8,-1 mov r9,-1 mov r10,-1 mov r11,-1 mov r12,-1 mov r13,-1 mov r14,-1 mov r15,-1 dec ecx jge .test3 jmp .out
我测试了他提到的以下几种实现方案:
mov e/rax, -1 xor eax, eax dec e/rax xor ecx, ecx lea e/rax, [rcx-1] or e/rax, -1
为简化表述,下文表格中使用group1 a (g1a)代指mov eax,-1,分组对应关系如下:
| 序号 | 指令模式 | 测试编号 |
|---|---|---|
| group1 a | mov eax,-1 | test 7 |
| group1 b | mov rax,-1 | test3 |
| group2 a | xor eax, eax / dec eax | test6 |
| group2 b | xor eax, eax / dec rax | test2 |
| group3 a | xor ecx, ecx / lea eax, [rcx-1] | test0 |
| group3 b | xor ecx, ecx / lea rax, [rcx-1] | test-1(test00) |
| group4 a | or eax,-1 | test5 |
| group4 b | or rax,-1 | test1 |
测试结果显示,第1到3组使用64位寄存器时,单次循环多消耗1个周期,IDQ_UOPS_NOT_DELIVERED指标也同步上升,请问这是否能精准解释单循环多1周期的现象?测试性能指标如下:
| 周期数 | MITE cycles(r1002479) | MITE 4uops cycles (r4002479) | IDQ UOPS NOT DELIVERED(r19c) | |
|---|---|---|---|---|
| g1a | 1,300,903,705 | 1,300,104,496 | 800,055,137 | 601,487,115 |
| g1b | 1,400,852,931 | 1,400,092,325 | 800,049,313 | 1,001,524,712 |
| g2a | 1,600,920,156 | 1,600,113,480 | 1,300,061,359 | 501,522,554 |
| g2b | 1,700,834,769 | 1,700,108,688 | 1,300,057,576 | 901,467,008 |
| g3a | 1,701,971,425 | 1,700,093,298 | 1,300,111,482 | 902,327,493 |
| g3b | 1,800,891,861 | 1,800,110,096 | 1,300,059,338 | 1,301,497,001 |
| g4a | 1,201,164,208 | 1,200,122,275 | 1,100,049,081 | 201,592,292 |
| g4b | 1,200,553,577 | 1,200,074,422 | 1,100,031,729 | 200,772,985 |
除此之外,g2a和g2b的执行端口分布存在差异,和g1组、g3组的表现不同(g1、g3组的32位/64位版本端口分布一致)。且注释掉times 32 nop后该差异现象消失,请问该现象是否和MITE有关?执行端口分布数据如下:
| p0 | p1 | p2 | p3 | p4 | p5 | p6 | p7 | |
|---|---|---|---|---|---|---|---|---|
| g1a | 299,868,019 | 300,014,657 | 5,925 | 7,794 | 16,589 | 300,279,232 | 499,885,294 | 7,242 |
| g1b | 299,935,968 | 300,085,089 | 6,622 | 8,758 | 18,842 | 299,935,445 | 500,426,436 | 7,336 |
| g2a | 299,800,192 | 299,758,460 | 7,461 | 9,635 | 20,622 | 399,836,486 | 400,312,354 | 8,446 |
| g2b | 200,047,079 | 200,203,026 | 7,899 | 9,967 | 21,539 | 500,542,313 | 500,296,034 | 9,635 |
| g3a | 36,568 | 550,860,773 | 7,784 | 10,147 | 22,538 | 749,063,082 | 99,856,623 | 9,767 |
| g3b | 36,858 | 599,960,197 | 8,232 | 10,763 | 23,086 | 700,499,893 | 100,078,368 | 9,513 |
| g4a | 200,142,036 | 300,600,535 | 5,383 | 6,705 | 15,344 | 400,045,302 | 500,364,377 | 6,802 |
| g4b | 200,224,703 | 300,284,609 | 5,464 | 7,031 | 15,817 | 400,047,050 | 499,467,546 | 6,746 |
测试环境:Intel i7-10700、Ubuntu 20.04、NASM 2.14.02。
问题解答
1. IDQ_UOPS_NOT_DELIVERED上升是否能解释单循环多1周期的现象
可以完全对应。你使用的10代酷睿(Comet Lake)的前端legacy解码通路(MITE)单周期最多只能输出4个uop,你添加32个nop的操作已经屏蔽了DSB(解码缓存)和LSD(循环流检测器),所有指令都走MITE实时解码。
1~3组的64位指令比32位多了1个REX前缀,单条指令长度从5字节变成7字节,13条置1指令合计多了26字节。MITE的取指窗口是16字节对齐,64位版本的指令流会跨更多取指窗口,导致每个循环刚好少传递4个uop,1亿次循环累计的IDQ_UOPS_NOT_DELIVERED增量刚好在4亿左右,和单循环多1周期的损耗完全匹配。
group4的or指令没有出现该差异,是因为or reg, imm的微架构解码优先级更高,长度差带来的取指压力被前端缓冲完全消化,没有出现uop供给不足的情况。
2. g2端口分布差异是否和MITE有关
确实和MITE直接相关。你添加32个nop后,整个循环的指令流对齐状态发生变化,MITE在解码xor-zero + dec的双指令序列时,会根据指令对齐状态给uop打上端口调度偏向标记。xor-zero和dec都可以在p0/p1/p5/p6四个端口执行,解码阶段的调度偏向会直接影响最终的端口分布。
当你去掉times 32 nop后,循环体积足够小,会被DSB或者LSD缓存,此时uop的端口分配由后端统一调度,不会受解码阶段的对齐影响,差异自然消失。g1是单指令立即数移动、g3是xor + lea组合,lea的可用端口固定为p1/p5,不受解码阶段调度偏向影响,所以32位和64位版本的端口分布没有差异,只有g2的双ALU通用指令组合会出现该现象。
内容的提问来源于stack exchange,提问作者moep0

