Extended asm - 注册约束行为异常?

Extended asm - Register constraints behaving oddly?

上下文

Linux 64 位。海湾合作委员会 4.8.2.

气体组装。 AT&T 语法。

我刚看完this answer

代码:

  int operand1, operand2, sum, accumulator;

  operand1 = 10; operand2 = 15;

  __asm__ volatile ("movl %1, %0\n\t"
           "addl %2, %0"
     : "=r" (sum)     /* output operands */
     : "r" (operand1), "r" (operand2) /* input operands */
     : "0");        /* clobbered operands */

  accumulator = sum;

  __asm__ volatile ("addl %1, %0\n\t"
     "addl %2, %0"
     : "=r" (accumulator)
     : "0" (accumulator), "r" (operand1), "r" (operand2)
     : "0");

编译时当然没有优化

我用 valgrind --tool=cachegrind ./my_bin

做了实验

实际上,如果我替换

"0" (accumulator), "r" (operand1), "r" (operand2)

"0" (accumulator), "m" (operand1), "m" (operand2)

我少了一个指令 == 一个 cpu 周期保存因为没有注册表操作

现在,替换

"0" (accumulator), "r" (operand1), "r" (operand2)

"r" (accumulator), "r" (operand1), "r" (operand2)

我也剃了 1 cpu 个周期。

所以

"r" (accumulator), "m" (operand1), "m" (operand2)

节省 2 cpu 个周期。

问题

1) 如果它们减慢速度,我们为什么要至少使用一个寄存器?真的有被覆盖的风险吗?

2) 为什么用“0”而不是 "r" 来减慢速度?这对我来说是不合逻辑的,因为我们只是引用相同的值(即累加器)。 GCC 不应输出不同的代码! "r" 可能意味着选择另一个寄存器 -> 废话 && 慢。

在不进入 asm tutorial 的情况下,我认为查看有无优化的代码生成可能会更好。我正在使用 OSX,它与 x86-64 Linux.

基本相同的 ABI

首先:您正在寻找 sum <- op1 + op2

其次是:acc <- sum; acc <- acc + op1 + op2
我们可以将其替换为:acc <- sum + op1 + op2;不需要:acc = sum;
(顺便说一句 - op1, op2 分别是 %2, %3%1 'aliases' %0

这仍然不是对内联汇编的特别有效的使用,只是为了将事情修复成可以检查的东西:

int test_fn (void)
{
    int op1 = 10, op2 = 15, sum, acc;

    __asm__ ("movl %k1, %k0\n\taddl %k2, %k0"
             : "=&r" (sum) : "r" (op1), "r" (op2));

    __asm__ ("addl %k2, %k0\n\taddl %k3, %k0"
             : "=r" (acc) : "0" (sum), "r" (op1), "r" (op2));

    return acc;
}

没有优化:gcc -Wall -c -S src.c(评论是我的)

        pushq   %rbp
        movq    %rsp, %rbp

        movl    , -4(%rbp)   # store 10 -> mem (op1)
        movl    , -8(%rbp)   # store 15 -> mem (op2)
# asm(1)
        movl    -4(%rbp), %edx  # load op1 -> reg (%1)
        movl    -8(%rbp), %ecx  # load op2 -> reg (%2)
        movl %edx, %eax         # mov %1 to %0
        addl %ecx, %eax         # add %2 to %0
        movl    %eax, -12(%rbp) # store %0 -> mem (sum)
# asm(2)
        movl    -12(%rbp), %eax # load sum -> reg (%1 = %0)
        movl    -4(%rbp), %edx  # load op1 -> reg (%2)
        movl    -8(%rbp), %ecx  # load op2 -> reg (%3)
        addl %edx, %eax         # add %2 to %0
        addl %ecx, %eax         # add %3 to %0
        movl    %eax, -16(%rbp) # store %0 -> mem (acc)

        movl    -16(%rbp), %eax # load acc -> return value.
        popq    %rbp
        ret

编译器没有努力将中间结果保存在寄存器中。它只是将它们保存回堆栈中的临时内存,并在需要时再次加载。不过,这很容易理解。

让我们将您的更改应用到 asm(2) 输入:"0" (sum), "m" (op1), "m" (op2)

        ...
# asm(2)
        movl    -4(%rbp), %eax  # load sum -> reg (%1 = %0)
        addl -12(%rbp), %eax    # add op1 (mem) to %0
        addl -16(%rbp), %eax    # add op2 (mem) to %0
        movl    %eax, -8(%rbp)  # store %0 -> mem (acc)
        ...

内存位置有点不同,但这并不重要。事实上 addreg <- reg + mem 的形式意味着我们不需要先加载到寄存器。所以它确实保存了一条指令,但我们仍在读取和写入内存。


经过优化:gcc -Wall -O2 -c -S src.c

        movl    , %edx
        movl    , %ecx
# asm(1)
        movl %edx, %eax
        addl %ecx, %eax
# asm(2)
        addl %edx, %eax
        addl %ecx, %eax

        ret

没有内存访问。一切都在寄存器中完成。这是最快的速度。没有缓存访问,没有主内存等。如果我们像在未优化的情况下那样应用更改以使用 "m" 约束:

        movl    , -8(%rsp)
        movl    , %ecx
        movl    , %edx
        movl    , -4(%rsp)
# asm(1)
        movl %edx, %eax
        addl %ecx, %eax
# asm(2)
        addl -8(%rsp), %eax
        addl -4(%rsp), %eax

        ret

我们回到强制使用内存。不必要地为 asm(2) 存储和加载操作数。并不是说 valgrind 错了——只是推断寄存器的使用是导致速度变慢的原因。