【问题标题】:LEA or ADD instruction?LEA 或 ADD 指令?
【发布时间】:2011-09-13 11:04:06
【问题描述】:

我在手写汇编的时候,一般会选择表格

lea eax, [eax+4]

在表格上方..

add eax, 4

我听说 lea 是“0-clock”指令(如 NOP),而“add”不是。但是,当我查看编译器生成的程序集时,我经常看到使用后一种形式而不是第一种形式。我足够聪明,可以信任编译器,所以任何人都可以阐明哪个更好?哪个更快?为什么编译器选择后者而不是前者?

【问题讨论】:

  • 如果任何指令实际上在做有用的工作,它怎么可能是“零时钟”指令?
  • 这是一条零时钟指令,因为所需的所有工作都在解码步骤中完成 - 当 CPU 解码指令时,无论如何都会从 MODRM/SIB 计算偏移量。至少这是我的理论。另外,我确切地知道 lea 指令是什么以及它的作用——我的问题是关于 lea 与 add,而不是 lea 与 mov(有很大的不同——你不能在不访问内存的情况下在 'mov' 中使用位移) .
  • 很久以前就是这样,回到最初的 Pentium。现代编译器为更晚的内核生成代码。手动优化机器代码的时代已经结束。
  • 它曾经很便宜(不是免费的),因为它在旧芯片上使用了专用的地址计算硬件,并为您购买了一些并行性。在当前的 CPU 上,两条指令可能会导致相同的微操作。
  • GCC 4.8 默认使用lealeamov: stackoverflow.com/questions/1699748/…

标签: assembly x86


【解决方案1】:

主要原因是下一个。如果您仔细查看 x86,您会注意到,这个 ISA 是两个地址。每条指令最多接受两个参数。因此,操作的语义是下一个:

DST = DST <operation> SRC

LEA是一种hack指令,因为它是x86 ISA中的SINGLE指令,实际上是三地址:

DST = SRC1 <operation> SRC2

它是一种hack指令,因为它重用了x86 CPU的arguments dispatcher电路来执行加法和移位。

编译器使用 LEA 是因为在加法寄存器的内容有利于保持不变的情况下,该指令允许他们用单个指令替换少量指令。请注意,在所有情况下,当编译器使用 LEA DST 寄存器不同于 SRC 寄存器或 SRC 参数利用复杂的地址计算逻辑时。

例如,在生成的代码中几乎不可能找到这样的用例:

LEA EAX, [EAX   ] // equivalent of NOP
LEA EAX, [ECX   ] // equivalent of MOV EAX, ECX
LEA EAX, [EAX+12] // equivalent of ADD EAX, 12

但接下来的用例很常见:

LEA EAX, [ECX      +12] // there is no single-instruction equivalent
LEA EAX, [ECX+EDX*4+12] // there is no single-instruction equivalent
LEA EDX, [ECX+EDX*4+12] // there is no single-instruction equivalent

事实上,假设应该保留 EBP 的值以供将来使用,设想下一个场景:

LEA EAX, [EBP+12]
LEA EDX, [EBP+48]

只有两条指令!但是在没有 LEA 的情况下,代码将是下一个

MOV EAX, EBP
MOV EDX, EBP
ADD EAX, 12
ADD EDX, 48

我相信使用 LEA 的好处现在应该很明显了。您可以尝试替换此指令

LEA EDX, [ECX+EDX*4+12] // there is no single-instruction equivalent

通过基于 ADD 的代码。

【讨论】:

  • LEA EAX, [EAX ] // equivalent of NOP 在 64 位模式下不是 NOP
  • Michael,您能澄清一下您提到的 IA-32 和 AMD64 之间的区别吗?
  • 在 64 位模式下,如果操作的目标是 32 位寄存器,CPU 会自动将结果归零,将结果扩展到整个 64 位寄存器。
  • 迈克尔,谢谢!我不知道这个功能。
【解决方案2】:

您可以像加法运算一样在同一个时钟周期内执行 lea 指令,但如果您将 lea 和 add 一起使用,您可以在一个周期内执行三个操作数的加法!如果您使用两个只能在 2 个时钟周期内执行的添加操作:

mov eax, [esp+4]   ; get a from stack
mov edx, [esp+8]   ; get b from stack
mov ecx, [esp+12]  ; get c from stack
lea eax, [eax+edx] ; add a and b in the adress decoding/fetch stage of the pipeline
add eax, ecx       ; Add c + eax in the execution stage of the pipeline
ret 12

【讨论】:

  • 错了,大多数超标量 CPU 可以同时运行多个加法指令。英特尔 Haswell 及更高版本每个时钟可以运行 4 个add 指令(如果这就是它的全部功能)。或者它可以每个时钟运行 2 个 ADD 和 2 个 LEA insns。此外,带有直接参数的ret 很慢。此外,如果您正在优化,您可以将其中一个负载折叠到 add 中。 add eax, [esp+12].
  • 不,没有错。仅仅因为这并不适用于市场上的每个 cpu,它并没有错。接下来我们不是在讨论返回策略,而是在讨论 add/lea 指令。如果使用'add eax,[esp+12]',我看不到任何性能优势,因为它不会绕过内存获取阶段。同样,我们不是在讨论如何从 a 获取数据到 b,而是在讨论使用 lea 而不是添加指令的优势。
  • 使用add eax, [esp+12] 的性能优势在于代码大小和指令/微指令的数量。整个函数可以是mov eax, [esp+4]/add eax, [esp+8]/add eax, [esp+12]/ret 12,它缩短了 2 条指令,因此解码速度更快,等等。不过,如果你正在针对 Atom 进行优化,你确实有道理,但确实如此AGU 中的 LEA 而不是 ALU,所以我认为add 可以以更低的延迟消耗lea 结果是对的。但是,lea 需要更快地准备好数据(在管道的早期阶段),而有序 Atom 无法提前完成前面的加载。
【解决方案3】:

x86 CPU 上LEAADD 之间的一个显着区别是实际执行指令的执行单元。现代 x86 CPU 是超标量的,并且有多个并行运行的执行单元,管道为它们提供有点像循环(barstalls)。问题是,LEA 由处理寻址的单元(其中一个)处理(发生在管道的早期阶段),而ADD 则进入 ALU(算术/逻辑单元) ,并且在管道中后期。这意味着超标量 x86 CPU 可以同时执行 LEA 和算术/逻辑指令。

LEA 通过地址生成逻辑而不是算术单元这一事实也是它曾经被称为“零时钟”的原因;它不需要任何时间来执行,因为地址生成已经发生在它应该/被执行的时候。

它不是免费,因为地址生成是执行管道中的一个步骤,但它没有执行开销。并且它不占用 ALU 管道中的插槽。

编辑:澄清一下,LEA 不是免费的。即使在不通过算术单元实现它的 CPU 上,由于指令解码/分派/退休和/或 所有 指令要经过的其他流水线阶段,它也需要时间来执行。对于通过地址生成实现它的 CPU,执行LEA 所花费的时间发生在管道的不同阶段

【讨论】:

  • @harold:您能否提供一些参考资料来说明您所说的“真实”是什么意思?从历史上看,LEAADD 以不同的方式(/由 CPU 中的不同单元)完成是正确的,即使在今天,英特尔的 CPU 对于LEAADD 仍然具有不同的延迟/吞吐量时序。跨度>
  • 历史上可能,但只要看看这里:agner.org/optimize/instruction_tables.pdf 并查看lea 的单位(通常是“alu”,几次是“agu”)和延迟(从不为零,有时超过 1)。更详细的时间安排(但分析较少):instlatx64.atw.hu
  • @harold:这正是参考......“AGU”==“地址生成单元”,我试图强调这一点。另请注意,我已明确表示它不是免费的,并将“零时钟”放在引号中。正如我所看到的,这里的问题主要是关于LEA 的开销发生在管道中的哪里 ...与ADD 相比。
  • 此答案仅适用于 AMD K8/K10。 (Intel P6/SnB/P4/Silvermont 和 AMD Bulldozer-family/Bobcat/Jaguar 都在其 ALU 上运行 LEA)。根据 Agner Fog 的表格,Atom(Silvermont 之前)和 Via Nano 等 CPU 在其 AGU 端口上运行 LEA,但延迟比 ADD 更差。只有 AMD k8/k10 在其 AGU 上以良好的性能运行 LEA,但即便如此,AGU 中的延迟仍为 2 个周期,而 K10 在 ALU 端口上运行的简单寻址模式为 1 个周期。
  • 这对于 非常 旧的 Intel CPU 也是正确的。肯定比 Pentium 4 更老,因为 P4 放弃了 AGU 的桶形移位器。 Pentium、Pentium Pro 和 Pentium II 在 AGU 中进行 LEA 计算,而不是在 ALU 上,正如这个答案最初建议的那样。这带来了很好的优化可能性。如果您知道如何利用它,LEA 在某些情况下实际上是免费的。
【解决方案4】:

LEA 并不比 ADD 指令快,执行速度是一样的。

但是LEA sometimes offer more than ADD。 如果我们需要结合第二个寄存器进行简单快速的加法/乘法运算,LEA 可以加快程序执行速度。 另一方面,LEA 不会影响 CPU 标志,因此 没有溢出检测的可能性。

【讨论】:

    【解决方案5】:

    我足够聪明,可以信任编译器,那么任何人都可以阐明哪个更好吗?

    是的,有一点。首先,我从以下消息中得到这个:https://groups.google.com/group/bsdnt-devel/msg/23a48bb18571b9a6

    在此消息中,开发人员优化了一些我写得很糟糕的程序集,以便在 Intel Core 2 处理器中快速运行。作为这个项目的背景,它是一个我和其他一些开发人员参与的 bsd bignum 库。

    在这种情况下,所有被优化的只是添加两个数组,如下所示:uint64_t* x, uint64_t* y。数组的每个“肢体”或成员都代表 bignum 的一部分;基本过程是从最不重要的肢体开始对其进行迭代,将这对添加并继续向上,每次都向上传递进位(任何溢出)。 adc 在处理器上为您执行此操作(我认为无法从 C 访问进位标志)。

    在那段代码中,使用了lea something, [something+1]jrcxz 的组合,这显然比我们以前使用的jnz/add something, size 对更有效。但是,我不确定这是否是由于简单地测试不同的指令而发现的。你得问问。

    但是,在稍后的消息中,它是在 AMD 芯片上测量的,性能并不那么好。

    我还了解到不同的操作在不同的处理器上执行的方式不同。例如,我知道 GMP 项目使用cpuid 检测处理器,并根据不同的架构传入不同的汇编例程,例如core2, nehalem.

    您必须问自己的问题是您的编译器是否为您的 cpu 架构生成优化的输出?例如,众所周知,英特尔编译器会执行此操作,因此可能值得测量性能并查看它产生的输出。

    【讨论】:

    • 非常好的响应 - 我不知道 lea 指令可能比 AMD CPU 上的 add 指令慢!我实际上使用的是 MSVC 10,但我使用的是 Intel CPU。
    • @Jakob 据我了解,这更像是 AMD k8 和 k10 在整数运算方面的速度非常快......但是,情况不一定如此。可能是 jrcxz 指令减慢了 AMD 的速度。优化汇编是一个我知之甚少的领域,但我的印象是你必须考虑整个算法,而不仅仅是一条指令。不过,我还是会等一等,SO 上有很多聪明的人,而且可能有人比我知道的更多。
    • 好的,我会等一下,看看还有什么其他答案可以咳出来。不过感谢您的回答!
    • 我已经检查了你的优化案例......这很糟糕,因为不需要使用 "lea rcx, [rcx+1]" 而不是 "inc rcx" 因为 inc 指令不影响携带标志,因为我在评论中红色了这个技巧的目的。
    • 我很好奇:第十个怎么了?
    猜你喜欢
    • 2012-02-27
    • 2012-02-27
    • 2011-05-30
    • 1970-01-01
    • 1970-01-01
    • 2020-09-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多