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)
...
内存位置有点不同,但这并不重要。事实上 add
和 reg <- 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 错了——只是推断寄存器的使用是导致速度变慢的原因。
上下文
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)
...
内存位置有点不同,但这并不重要。事实上 add
和 reg <- 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 错了——只是推断寄存器的使用是导致速度变慢的原因。