Assembly why is "lea eax, [eax + eax*const]; shl eax, eax, const;" combined faster than &q

互联网 行业动态 更新时间:2024-06-13 00:19:06

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)。这就是为什么leaimul/快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+jnesub+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

本文发布于:2023-04-20 20:23:54,感谢您对本站的认可!
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:eax   const   lea   Assembly   quot

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!