gcc 在ARM上将间接跳转编译为两条指令而不是一条指令有什么原因吗?

7xllpg7q  于 2022-12-29  发布在  其他
关注(0)|答案(1)|浏览(145)

给定以下小程序:

#include <stdlib.h>

#define NEXT goto **ip++
#define guard(n) asm("#" #n)

int main() {
  static void  *prog[] = {&&next1,&&next2,&&next1,&&next3,&&next1,&&next4,&&next1,&&next5,&&next1,&&loop};
  void ** ip=prog;
  int    count = 100000000;
  NEXT;

 next1: guard(1); NEXT;
 next2: guard(2); NEXT;
 next3: guard(3); NEXT;
 next4: guard(4); NEXT;
 next5: guard(5); NEXT;
 loop:
  if (count) {
    count--;
    ip=prog;
    NEXT;
  }
  exit(0);
}

我注意到后面的每个#语句都被编译为两条指令。

ldr     r2, [r3], #4
        mov     pc, r2    @ indirect register jump

我本以为这只需要一个指令:

ldr pc, [r3], #4

我在这里找到了讨论:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=40887
“问题在于指令“ldr pc,[r3,#0]”未被Cortex-A8的分支预测器视为函数调用,如DDI 0344 J第5.2.1节“返回堆栈预测”所述。因此,被调用函数的返回预测错误,导致与直接调用相比损失13个周期。”
然而,“后藤”不是一个函数调用,并且在这里根本不需要返回堆栈。
我想知道这是否是一些优化,GCC和CLANG都错过了或后者是一个更差的表现,原因我不知道?

emeijp43

emeijp431#

这看起来像是错过了优化,除非有某种 * 其他 * 微体系结构原因需要在其他CPU上避免它。(这是有道理的,但我不会特别期待它。提前将多条指令加载到寄存器中可能给予一些时间来隐藏加载使用延迟,并减少可能的误预测损失,但加载前一条指令可能无关紧要,除非ldr在PC中有特殊之处。)

你是对的,bug #40887只是关于blx的间接调用与手动设置返回地址和跳转。它与函数内部的间接跳转无关,比如switch或计算后藤。(除非GCC通常避免加载到PC,所以错过的优化是修复该bug的附带损害。
而且你没有使用volatile,这是另一个经常让GCC用一个单独的指令来执行加载,而不是把它合并到其他指令中的原因(比如避免x86 add eax, [rdi]。或者在这种情况下,像ARM加载到PC这样的内存源跳转,它可能会认为这是特殊的)。
比较-mcpu=cortex-a8-marm-mthumb,我们可以看到GCC在thumb模式下确实需要额外的指令来设置mov pc,reg之前的目标地址的低位。https://godbolt.org/z/87EszvWP1
或者,这也可能是一个遗漏的优化:只有ldr pc, [mem]会停留在当前模式,并且我们知道我们在单个函数内跳转,因此不可能 * 更改 * 模式。和/或如果使用bx r2实际上更快,则跳转表可以使用已设置的低位构建。
https://developer.arm.com/documentation/dui0473/m/arm-and-thumb-instructions/ldr--register-offset-
对于字加载,Rt可以是PC。加载到PC会导致分支到加载的地址。在ARMv 4中,加载地址的位[1:0]必须为0 b 00。在ARMv 5 T及以上版本中,位[1:0]不得为0 b10,如果位[0]为1,则执行继续处于Thumb状态,否则执行继续处于ARM状态。
在Thumb模式下,ldr到PC只能使用32位指令,但是ldr到r 0 -7和转移到r 0 - 7可以都是16位指令,但是我怀疑这是否会更好,除非您可以更早地调度加载。

相关问题