Jér*_*ard 6
您可以在这里和那里看到大多数主流架构的指令成本。基于此并假设您使用例如英特尔 Skylake 处理器,您可以看到imul
每个周期可以计算一个 32 位指令,但延迟为 3 个周期。在优化后的代码中,lea
每个周期可以执行 2 条指令(非常便宜),延迟为 1 周期。同样的事情也适用于sal
指令(每个周期 2 个,延迟 1 个周期)。
这意味着优化版本只需 2 个延迟周期即可执行,而第一个需要 3 个延迟周期(不考虑相同的加载/存储指令)。此外,第二个版本可以更好地流水线化,因为由于超标量乱序执行,两条指令可以针对两个不同的输入数据并行执行。请注意,两个加载也可以并行执行,尽管每个周期只能并行执行一个存储. 这意味着执行受限于存储指令的吞吐量。总体而言,每个周期只能计算 1 个值。AFAIK,最近的 Intel Icelake 处理器可以像新的 AMD Ryzen 处理器一样并行执行两个存储。第二个预计在所选用例(英特尔 Skylake 处理器)上同样快或可能更快。在最近的 x86-64 处理器上,它应该明显更快。
请注意,该lea
指令非常快,因为乘加是在专用 CPU 单元(硬连线移位器)上完成的,并且它仅支持乘法的某些特定常数(支持的因子是 1、2、4 和 8,这意味着lea 可用于将整数乘以常量 2、3、4、5、8 和 9)。这就是为什么lea
比imul
/快mul
。
更新(v2):
我可以使用 GCC 11.2(在具有 i5-9600KF 处理器的 Linux 上)重现较慢的执行。-O2
速度变慢的主要来源是版本中要执行的微操作(uops)数量较多,当然还有一些执行端口的饱和,这肯定是由于微操作调度不好造成的。-O2
这是循环的组装-Os
:
1049: 8b 15 d9 2f 00 00 mov edx,DWORD PTR [rip+0x2fd9] # 4028 <a>
104f: 6b d2 24 imul edx,edx,0x24
1052: 89 15 d8 2f 00 00 mov DWORD PTR [rip+0x2fd8],edx # 4030 <res>
1058: 48 ff c8 dec rax
105b: 75 ec jne 1049 <main+0x9>
这是循环的组装-O2
:
1050: 8b 05 d2 2f 00 00 mov eax,DWORD PTR [rip+0x2fd2] # 4028 <a>
1056: 8d 04 c0 lea eax,[rax+rax*8]
1059: c1 e0 02 shl eax,0x2
105c: 89 05 ce 2f 00 00 mov DWORD PTR [rip+0x2fce],eax # 4030 <res>
1062: 48 83 ea 01 sub rdx,0x1
1066: 75 e8 jne 1050 <main+0x10>
现代 x86-64 处理器解码(可变大小)指令,然后将它们转换为(更简单的固定大小)微操作,最终在多个执行端口上执行(通常并行)。有关特定 Skylake 架构的更多信息,请参见此处。Skylake 可以将多条指令宏融合成一个微操作。在这种情况下,dec
+jne
和sub
+jne
指令在每种情况下都融合到一个微指令中。这意味着-Os
版本执行 4 微指令/迭代,而版本-O2
执行 5 微指令/迭代。
微指令存储在称为解码流缓冲区(DSB)的微指令缓存中,因此处理器不需要再次解码/翻译(小)循环的指令。要执行的缓存微指令在称为指令解码队列 (IDQ) 的队列中发送。最多可以从 DSB 向 IDQ 发送 6 个微指令/周期。对于该-Os
版本,每个周期仅将 4 微秒的 DSB 发送到 IDQ(可能是因为循环受饱和的存储端口限制)。对于该-O2
版本,DSB 的 5 微指令仅在每个周期发送到 IDQ,但 5 次中有 4 次(平均)!这意味着每 4 个周期添加 1 个延迟周期,导致执行速度降低 25%。这种影响的原因尚不清楚,似乎与 uops 调度有关。
然后将 Uops 发送到资源分配表 (RAT) 并发布到预留站 (RS)。RS将微指令分派到执行它们的端口。然后,微指令被退役(即承诺)。从 DSB 间接传输到 RS 的微指令数量对于两个版本都是恒定的。相同数量的微指令被淘汰。但是,在两个版本中,RS 每个周期(并由端口执行)都会再调度 1 个 ghost uop。这可能是用于计算存储地址的微指令(因为存储端口没有自己的专用 AGU)。
这是从硬件计数器收集的每次迭代的统计信息(使用perf
):
1049: 8b 15 d9 2f 00 00 mov edx,DWORD PTR [rip+0x2fd9] # 4028 <a>
104f: 6b d2 24 imul edx,edx,0x24
1052: 89 15 d8 2f 00 00 mov DWORD PTR [rip+0x2fd8],edx # 4030 <res>
1058: 48 ff c8 dec rax
105b: 75 ec jne 1049 <main+0x9>
以下是整体端口利用率的统计数据:
1050: 8b 05 d2 2f 00 00 mov eax,DWORD PTR [rip+0x2fd2] # 4028 <a>
1056: 8d 04 c0 lea eax,[rax+rax*8]
1059: c1 e0 02 shl eax,0x2
105c: 89 05 ce 2f 00 00 mov DWORD PTR [rip+0x2fce],eax # 4030 <res>
1062: 48 83 ea 01 sub rdx,0x1
1066: 75 e8 jne 1050 <main+0x10>
端口 6 只是-O2
版本上完全饱和的端口,这是出乎意料的,这当然解释了为什么每 5 个周期需要一个额外的周期。请注意,只有与指令关联的微指令(shl
同时sub+jne
)使用端口 0 和 6(没有其他端口)。
请注意,由于停顿周期,总共 480% 是调度工件。实际上,6*4=24
微指令应该每 5 个周期执行一次 ( 24/5*100=480
)。另请注意,5 个周期中的 1 个不需要存储端口(平均每 5 个周期执行 4 次迭代,因此 4 个存储 uop),因此它的使用率为 80%。
有关的:
LEA 指令的目的是什么? 为什么我的循环包含在一个缓存行中时要快得多? 现代英特尔处理器有多少种超标量方式? en.wikipedia./wiki/Superscalar_processor更多推荐
eax,const,lea,Assembly,quot
发布评论