【问题标题】:Why gcc (ARM) aren't using Global Register Variables as source operands?为什么 gcc (ARM) 不使用全局寄存器变量作为源操作数?
【发布时间】:2016-08-06 17:22:22
【问题描述】:

这是一个c源代码示例:

register int a asm("r8");
register int b asm("r9");

int main() {
    int c;
    a=2;
    b=3;
    c=a+b;
    return c;
}

这是使用 arm gcc 交叉编译器生成的汇编代码:

$ arm-linux-gnueabi-gcc  -c global_reg_var_test.c -Wa,-a,-ad

...
mov     r8, #2
mov     r9, #3
mov     r2, r8
mov     r3, r9
add     r3, r2, r3
...

使用 -frename-registers 时,行为是相同的。 (更新。之前我用 -O3 说过。)

所以问题是:为什么 gcc 添加第 3 和第 4 个 MOV 而不是 'ADD R3, R8, R9'?

上下文:我需要在不重命名寄存器的模拟中序 cpu (gem5 arm minorcpu) 中优化代码。

【问题讨论】:

  • @user3528438:对我来说是movs r0, #5; bx lr
  • @GermanoAndersson:你能制作出更好的测试用例吗?当我只能看到 gcc -O0 的这种行为时,很难说 gcc 没有很好地优化这一点。
  • 如果您需要比 GCC 提供的更好的优化,您将编写自定义程序集。试图强迫它使用某些寄存器是行不通的。整个 register asm("") 东西是使用更简单的寄存器分配器的古代编译器版本的遗留物。
  • 大多数 ARM 内核不会重新排序寄存器。只需使用正确的目标架构。一般来说,智取编译器是个坏主意。在不干扰优化器的情况下得到什么代码?
  • 将OP源的当前版本复制到godbolt,我们看到使用-O2,r8和r9得到2和3赋值,r0直接赋值5。 r2 和 r3 没有多余的分配。不能删除对 r8 和 r9 的分配,因为(正如文档所说)“存储到 [globals] 中的存储即使看起来已经死了也不会被删除。”我看不出这怎么能更有效率。

标签: c gcc assembly arm cpu-registers


【解决方案1】:

我举了一个真实的例子(张贴在 cmets)和put it on the godbolt compiler explorercalc() 的主要低效之处在于 src1src2 是必须从内存加载的全局变量,而不是在寄存器中传递的 args。

我没看main,只看calc

register int sum asm ("r4");
register int r asm ("r5");
register int c asm ("r6");
register int k asm ("r7");
register int temp1 asm ("r8");    // really?  you're using two global register vars for scratch temporaries?  Just let the compiler do its job.
register int temp2 asm ("r9");
register long n asm ("r10");
int *src1, *src2, *dst;

void calc() {
  temp1 = r*n;
  temp2 = k*n;

  temp1 = temp1+k;
  temp2 = temp2+c;

  // you get bad code for this because src1 and src2 are globals, not args passed in regs
  sum = sum + src1[temp1] * src2[temp2];
}

    # gcc 4.8.2 -O3 -Wall -Wextra -Wa,-a,-ad -fverbose-asm
    mla     r0, r10, r7, r6          @ temp2.9, n, k, c   @@ tmp = k*n + c
    movw    r3, #:lower16:.LANCHOR0  @ tmp136,
    mla     r8, r10, r5, r7          @ temp1, n, r, k     @@ temp1 = r*n + k
    movt    r3, #:upper16:.LANCHOR0  @ tmp136,
    ldmia   r3, {r1, r2}             @ tmp136,,           @@ load both pointers, since they're stored adjacently in memory
    mov     r9, r0                   @ temp2, temp2.9     @@ This insn is wasted: the first MLA should have had this as the dest
    ldr     r3, [r1, r8, lsl #2]     @ *_22, *_22
    ldr     r2, [r2, r9, lsl #2]     @ *_28, *_28
    mla     r4, r2, r3, r4           @ sum, *_28, *_22, sum
    bx      lr                       @

由于某种原因,整数乘加 (mla) 指令之一使用 r8 (temp1) 作为目标,但另一个写入 r0(暂存寄存器),并且仅稍后将结果移动到r9 (temp2)。

sum += src1[temp1] * src2[temp2] 是通过读取和写入 r4 (sum) 的 mla 完成的。

为什么需要 temp1temp2 成为全局变量?这只会阻止优化器进行激进的优化,这些优化不会计算出与 C 源代码完全相同的临时变量。幸运的是,C 内存模型足够弱,它应该能够对它们的分配重新排序,尽管这实际上可能是它没有直接将 MLA 转换为 temp2 的原因,因为它决定首先进行计算。 (嗯,内存模型是否适用?其他线程根本看不到我们的寄存器,所以这些全局变量实际上都是线程局部的。它应该允许对全局变量的分配进行宽松的排序。信号处理程序可以看到这些全局变量,并且可以在任何时候运行。gcc 没有遵循严格的源顺序,因为在源中,两个乘法都发生在任何一个相加之前。)

Godbolt 没有更新的 ARM gcc 版本,所以我不能轻易测试更新的 gcc。较新的 gcc 可能会在这方面做得更好。


顺便说一句,I tried a version of the function using local variables for temporaries, and didn't actually get better results。可能是因为仍然有太多寄存器全局变量,gcc 无法为临时变量选择方便的寄存器。

// same register globals, except for temp1 and temp2.

void calc_local_tmp() {
  int t1 = r*n + k;
  sum += src1[t1] * src2[k*n + c];
}
    push    {lr}                      @ gcc decides to push to get a tmp reg
    movw    r3, #:lower16:.LANCHOR0   @ tmp131,
    mla     lr, r10, r5, r7           @ tmp133, n.1, r, k.2
    movt    r3, #:upper16:.LANCHOR0   @ tmp131,
    mla     ip, r7, r10, r6           @ tmp137, k.2, n.1, c
    ldr     r2, [r3]                  @ src1, src1
    ldr     r0, [r3, #4]              @ src2, src2
    ldr     r1, [r2, lr, lsl #2]      @ *_10, *_10
    ldr     r3, [r0, ip, lsl #2]      @ *_20, *_20
    mla     r4, r3, r1, r4            @ sum, *_20, *_10, sum
    ldr     pc, [sp], #4              @

使用-fcall-used-r8 -fcall-used-r9 编译没有帮助; gcc 编写了相同的代码来推送lr 以获得额外的临时代码。它无法使用ldmia (load-multiple),因为它对将哪个临时放入哪个 reg 做出了次优选择。 (&src1 in r0 将让它加载 src1src2r2r3。)

【讨论】:

  • 感谢彼得!主要目标是优化不重命名寄存器的模拟中序 cpu (gem5 arm minorcpu) 中的代码。这个 CPU 有一个有限的组织(例如,只有一个乘法器),所以尝试增加这个组织并放置两个独立的指令块(集),但是如果没有重命名寄存器,我需要强制寄存器独立。此处发布的代码是为了澄清问题而改编的,最初没有 calc 函数 e 变量是本地的。
  • @GermanoAndersson:你真的得到了-O3 -frename-registers的次优代码吗? gcc 手册页说该选项确实针对没有硬件注册重命名的 CPU 进行了优化。也许还可以使用-mcpu=something_similar 为现有的有序 CPU 进行 gcc 调整,而无需重新命名。
  • 使用 -O3 gcc 将 MOVs+MUL 更改为 MLA,因此我对次要 cpu 中组织更改的优化没有效果。我需要两个没有依赖关系的指令块,当我向组织添加乘数时会导致并行运行。
  • @GermanoAndersson:没有MLA 指令的现有ARM CPU 是不是没有?通常,您不能通过为不同的架构制作未优化的代码来为您的目标架构制作优化的代码! gcc 使用mla 甚至-O2,这是您应该考虑的最低的普遍可接受的优化级别。 gcc 是开源的,因此理论上您可以在自定义 gcc 版本中修改 ARM 的机器定义。
  • @gma: 显然是MLA is baseline,所以没有-march 可以用来告诉gcc 不要在ARM 模式下使用它。在 ARMv6T2 之前它在 thumb 模式下不可用,所以 you can avoid it with -march=armv5t -mthumb,但是 asm 只使用两个寄存器指令(例如 mov r0, r10 / mul r0, r5)。是拇指 1 吗?我不是 ARM 专家。
猜你喜欢
  • 2015-02-06
  • 2017-09-20
  • 2019-09-27
  • 2017-05-25
  • 1970-01-01
  • 2021-10-13
  • 1970-01-01
  • 2021-01-22
  • 2012-01-18
相关资源
最近更新 更多