带有 aapcs 的 GCC 内联汇编

GCC inline asm with aapcs

我正在尝试使用 GCC 和 ARM Cortex-A7 优化编写内联汇编的数学函数。 我的代码是这样的:

__inline int __attribute__((pcs("aapcs"))) optAbsVal(int x)
{
  asm("CMP R0, #0\n"
      "IT LT\n"
      "RSBLT R0, R0, #0");
  return(x);
}

我没有指定任何 input/output 参数,也没有指定内联 asm 块内的 clobbers,因为根据调用约定,x 应该在 R0 中,return 值也是如此。 问题是这个函数 returns 没有修改 x 的值,这让我觉得要么 x 不在 R0 中,要么编译器以某种方式修改了函数。 我通过添加参数 "=r"(x) : "0"(x) 解决了这个问题,但我仍然对这段代码不满意,因为我似乎在做不必要的操作。 我做 pcs("aapcs") 的原因是为了避免 load/store ops 以获得更好的性能,但这反而变得更糟。

因为 x 不是 return 值,所以它不需要在 R0 中。 return 值是对 return 语句中给出的表达式求值的结果。因此,对于 return x,return 值不是 x,return 值是 x 的值。这是一个重要的区别,因为这意味着 x 不需要存在于 R0 中,只是 x 中的值需要在函数 returns 之前复制到 R0 中。

因此,由于函数中要执行的最后一条语句是 return (x); 那么这意味着函数所做的最后一件事是将 x 复制到 R0,这破坏了存储在 R0 中的值你的内联汇编语句。

这就是为什么您必须始终完整地描述您的内联汇编语句对机器状态的影响。编译器不知道您要保留 R0 中的值。它不知道您希望 x 参数中传递的值在进入 asm 语句时位于 R0 中。由于调用约定,这可能是正确的,但调用约定的规则仅适用于函数的入口和出口,而不适用于 asm 语句所在的函数中间。如果您的函数内联到另一个函数中,则调用约定根本不适用,因为没有实际的函数调用。

所以你想要的是这样的:

__inline int optAbsVal(int x)
{
  asm("CMP %0, #0\n"
      "IT LT\n"
      "RSBLT %0, %0, #0"
      : "+r" (x) : : "cc");
  return(x);
}

内联汇编在这里完全没有意义。 GCC 已经知道如何优化绝对值,并且在内联 asm 中向编译器隐藏该过程将使您的代码优化得更糟,而不是更好。 https://gcc.gnu.org/wiki/DontUseInlineAsm

在纯 C 中编写绝对值至少总是一样好(除非编译器决定在内联后生成分支代码,并且分析表明分支是错误的选择。)

absval(int x) {
    return x<0 ? -x : x;  // ternary often compiles branchlessly
}

优于内联汇编的优点包括:编译器知道结果是非负的,并且可以相应地进行优化。例如,它可以通过简单的右移除以 2 的幂,而无需考虑与 C 符号除法的不同舍入:

void foo_asm (int *arr, int len) {
    for (int i=0 ; i<1024 ; i++){
      arr[i] = optAbsVal(arr[i]) / 4;  // Using Ross's correct implementation
    }
}

内循环(from gcc6.3 -O3 -mcpu=cortex-a7 -mthumb on the Godbolt compiler explorer):

.L4:
    ldr     r3, [r2, #4]
    CMP r3, #0             @@@@ Inline asm version
IT LT
RSBLT r3, r3, #0
    adds    r1, r3, #3
    bics    r3, r3, r3, asr #32
    it      cs
    movcs   r3, r1           @ x = x<0 ? x+3 : x  (I think, I didn't look up BICS)
    asrs    r3, r3, #2       @ x >>= 2
    str     r3, [r2, #4]!
    cmp     r2, r0
    bne     .L4

对比

void foo_pure (int *arr, int len) {
    for (int i=0 ; i<1024 ; i++){
      arr[i] = absval(arr[i]) / 4;  // Using my pure C
    }
}

.L8:               @@@@@@@@ Pure C version
    ldr     r3, [r2, #4]
    cmp     r3, #0           @ gcc emitted exactly your 3-insn sequence on its own
    it      lt
    rsblt   r3, r3, #0
    asrs    r3, r3, #2       @ non-negative division by 4 is a trivial >> 2
    str     r3, [r2, #4]!
    cmp     r1, r2
    bne     .L8

知道有符号变量是非负的对于编译器通常很有价值。signed overflow is undefined behaviour, so it's allowed to ignore the fact that 0 - 0x80000000 = 0x80000000, i.e. that -INT_MIN still has its sign bit set, because -INT_MIN is UB. The most negative number 是 2 的补码的特例。)


gcc 可以通过查看先前指令已经设置的标志而不是执行 cmp 来做得更好。 (这也可以为有序内核提供更好的指令调度)。

但是 absval(100 + arr[i]) 我看到了

    adds    r3, r3, #100
    cmp     r3, #0
    it      lt
    rsblt   r3, r3, #0

而不是使用 the sign flag alone for the MInus condition.

    @ hand-written, IDK why gcc doesn't do this, probably missed optimization:
    adds    r3, r3, #100    # set flags
    it      MI              # use the MInus condition instead of LessThan
    rsbmi   r3, r3, #0

内联 asm 也无法利用 ARM 的 3 操作数指令。 rsb 可能会在与输入不同的寄存器中产生结果(至少在 ARM 模式下,并且在统一语法中 IT 不需要拇指模式)。但是,如果您希望您的 asm 在 Thumb 模式下仍然 assemble,则不能只对 x 使用单独的输出操作数,而 rsb r1, r0, #0 不会 assemble.

而且,内联 asm 会阻止常量传播。 optAbsVal(-1) 编译为 4 条指令以在 运行 时翻转它。 absval(-1) 编译为 1.

的编译时常量

在带有 NEON 的目标上,内联汇编也无法自动矢量化。它还可能使编译器在本来应该展开的情况下不会展开循环。