程序集优化函数调用,允许从函数中取出常量?

Assembly optimizing function call, taking constants out of function allowed?

我有一个递归 C 函数

foo (int numFruits) {
   ....
   // recurse at some point
  } 

在主函数中。

对应的程序集如下所示:

.pos 0x500
main:
   %r10  // contains numFruits
   call foo
   halt

.pos 0x4000
foo: // recursive
  irmovq , %r13 // load value 8 into %r13
  ...

在 foo 内部,我对 quad 的大小使用了常量值,即 8 个字节长。 (C 代码中不存在值 8,但我使用此值将数组的长度转换为相应的地址等...)

如果每次递归调用 foo 时我都加载这个值,我认为这是在浪费周期。我想知道编译器是否能够优化它,以便在调用 main 中的 foo 之前加载常量?

示例:在调用 foo 之前将值 8 加载到 r13 一次,这样就不必每次都加载它。 (前提是 r13 恢复到加载值 8 之前的原始状态,在 hit halt 之后)

如果我在 main 之前将值 8 保存到 r13 中,这是否仍然保留了 foo(int numFruits) 的精神,或者我的更改是否等同于 foo(int numFruits, int quadSize)?

谢谢

相当于foo(int numFruits, long quadSize)。好吧,如果你的 y86 ABI 有 64 位 int,也许 int quadSize。所有正常的 x86-64 ABI 都有 32 位 int,Windows x64 甚至有 32 位 long.

您还标记了这个 x86。 x86-64 可以使用 5 字节指令将 8 移动到 64 位寄存器中,例如 mov , %r13d:1 个操作码字节 + imm32。 (实际上是 6 个字节,包括 REX 前缀)。对于不适合零或符号扩展的 32 位立即数的常量,您只需要 mov r64, imm64。写入 32 位寄存器零扩展到完整的 64 位寄存器。您甚至可以以牺牲速度为代价,编写更多代码高尔夫常量设置。像 push $imm8 / pop %r13 3 个字节(实际上是 4 个 REX 前缀)。优化代码大小时,您希望避免使用 r8..r15。 https://codegolf.stackexchange.com/questions/132981/tips-for-golfing-in-x86-x64-machine-code/132985#132985.

我不知道 y86 是否具有针对小常量的高效机器码编码。

据我所知,不存在任何物理 y86 CPU。有模拟器,但我不确定是否有任何 y86 硬件的虚拟设计(如 verilog)可以在周期精确的模拟器中模拟。

所以任何关于 "saving cycles" 的讨论都是对 y86 的延伸。 真正的 x86-64 CPU 是具有乱序执行的流水线超标量,并且通常不会在代码获取上遇到瓶颈。特别是在带有 uop 缓存的现代 CPU 中。根据循环的不同,关键路径外的额外 mov-immediate 指令可能不会减慢速度。 https://agner.org/optimize/, and see performance links in the x86 tag wiki.


但是,是的,您通常应该将常量设置提升到循环之外。

如果你的 "loop" 是递归的,你无法在没有昂贵的 call / ret 的情况下轻松优化到正常循环,你当然可以为 [= 创建一个包装函数70=] 用法,并让它进入一个私有函数,该函数有效地使用自定义调用约定(假定 %r13 = 8)。

.globl foo
foo:
    irmovq  , %r13

 # .p2align 4    # optional 16-byte alignment for the recursion entry point
 # fall through

.Lprivate_foo:
   # only reachable with r13=8
 # blah blah using r13=8
    call .Lprivate_foo
 # blah blah still assuming r13=8
    call .Lprivate_foo
 # more stuff
    ret                      # the final return 

没有别的可以调用private_foo;它是一个本地标签 (.Lxxx),只能从此来源看到。所以 .Lprivate_foo 的主体可以假设 R13 = 8.

如果 r13 是您的 y86 调用约定中的调用保留寄存器(就像在 x86-64 系统 V 中一样),则选择一个调用破坏寄存器,如 r11,或者让 public 包装函数 call private_foo 因此它可以在返回前恢复调用者的 r13。使用通常允许函数破坏的寄存器使这种额外开销接近于零,而不是引入额外的 call/ret.

级别

但这只有在您不从递归函数内部调用任何其他未知函数时才有效,否则您必须假设它们破坏了 R11。

将递归优化到循环中有很大的优势,编译器会尽可能地这样做。 (在像树遍历这样的双递归函数中,他们经常会将第2次递归调用变成循环分支,但对于非尾递归实际上仍然递归。)


如果您只是使用 8 作为比例因子,我担心您使用的是乘法。使用 3 的移位效率更高。或者(因为你标记了这个 x86 和 y86),也许使用缩放索引寻址模式。但如果它用于指针增量,那么真正的 x86 将使用 add-immediate。与 add , %rsi 一样,使用 add r/m64, imm8 encoding 仅使用 1 个字节作为常量(符号扩展为 64 位)。

但是 x86 等效项是 SIMD 向量常量或浮点常量,因为它们没有直接形式。在那种情况下,是的,您确实希望在循环外的寄存器中设置常量。