我正在调试一些汇编代码,在阅读了一些文档之后,我不确定我是否100%理解约束。我想知道是否有人可以让我挺直。如果我有以下代码(arm32):
int foo(int in1, int *ptr1) {
int out1=123;
asm volatile (
" cmp %[in1], #0;"
" bne 1b;"
" dmb;"
" mov %[out1], #0;"
"1: strex %[in1], [%[ptr1]];"
: [out1]"=r", [ptr1]"+r"(ptr1),
: [in1]"r"(in1),
: "memory" );
return out1;
}
我不清楚几件事:首先,我将out1
标记为输出,但是只有in1
为零时,它才是输出。我担心=r
约束被解释为“此值总是 设置”,从而告诉优化器任何先前的值都不相关。当然,我不确定如何为可能更改的内容写一个约束...
我也关心ptr1
。指针本身实际上并没有设置,但是指向的却是。我想知道这是否应具有读取约束,并想知道是否有适当的方法来设置此约束。
请注意,我在多个编译器(gcc和clang,以及每个编译器的不同版本)上使用此代码,因此我想避免对特定优化器进行任何假设。
答案 0 :(得分:3)
是的,"=r"
表示只写。寄存器在输入时无效。编译器不会费心在asm之前的选定寄存器中放入任何特定内容,因为它将被覆盖。编译器将进行优化,就像您在嵌入式asm之外编写out1 = asm_result;
一样。
"+r"
是输入/输出操作数。如果可能被修改,则您需要编译器假定它一直存在。
查看该函数的编译器生成的asm,例如在Godbolt编译器资源管理器上。 (https://godbolt.org/)。您可以查看编译器围绕内联汇编生成的代码,包括内联到另一个函数之后。
我也关心
ptr1
。指针本身实际上并没有设置,但是它指向的是。
是的,您值得关注。 "+r"(ptr1)
告诉编译器指针值已修改,但 not 并不暗示指向的值已修改。 "memory"
破坏符是执行此操作的一种繁重方法,或者就像Jester所说的那样,您应该只使用"=m"(*ptr1)
约束来让编译器选择寻址模式,并告诉它指向的内存是无条件写的。
在没有前面的LDREX的情况下STREX甚至有意义吗?我不这么认为,但是如果我错了,那么您只需要为该一条指令使用内联汇编,因为ARM编译器甚至对原子存储也使用普通的str
。
如果此函数执行LL / SC的后半部分,那么这很奇怪。
您确定无法使用内置的__atomic_store(ptr1, value, __ATOMIC_RELAXED)
+可选挡板或C11 atomic_store_explicit
做您想要的吗?
#include <stdatomic.h>
int foo(int in1, int *ptr1) {
int out1=123;
if (in1 != 0) {
out1 = 0;
//asm("dmb" ::: "memory");
atomic_thread_fence(memory_order_release); // make the following stores release-stores wrt. earlier operations
}
atomic_store_explicit((_Atomic int*)ptr1, in1, memory_order_relaxed);
return out1;
}
使用gcc6.3,on the Godbolt compiler explorer进行编译:
@ gcc6.3 -O3 -mcpu=cortex-a53 (ARM mode)
foo:
subs r3, r0, #0 @ copy in1 and set flags from it at the same time
moveq r0, #123 @ missed-optimization: since we still branch, no point hoisting this out of the if with predication
bne .L5
str r3, [r1] @ if()-not-taken path
bx lr
.L5:
dmb ish @ if()-taken path
mov r0, #0 @ makes the moveq doubly silly, because we do it again inside the branch.
str r3, [r1]
bx lr @ out1 return value in r0
因此它运行的指令与实现相同(除了str而不是strex),但是使用尾部重复的方式进行分支,并且可能节省了整体指令(可能具有较大的代码大小,但动态性较低 指令计数,因为我们使用了-O3
。)在-Os
中,我们获得了非常紧凑的asm,它更像是您的嵌入式asm(跳过mov和dmb
)。
Clang使用itte
(在拇指模式下)断定dmbne sy
来使整个事物变得无分支。 (请参阅其在Godbolt上的输出。)
请注意,如果要将单独的障碍移植到AArch64,则效率通常较低。您希望编译器能够使用AArch64的stlr
版本存储(即使它是顺序版本,而不是较弱的普通版本)。 dmb ish
是一个完整的内存屏障。另外,ARMv8的32位代码可以使用stl
。
请注意,完整的dmb
以后将订购 other 存储wrt。较早的存储,因此这在AArch64(或带有ARMv8指令的32位)上并不完全等效,在AArch64上,编译器生成的代码未使用dmb
。
此版本可编译为适用于所有体系结构的非常不错的asm:我看到的一个错失的优化是编译器无法将dmb
与str
分开,在条件str
之后留下一个公用dmb
。 (对于必须使用dmb
的情况。)
// recommended version
int foo_ifelse(int in1, int *ptr1) {
int out1=123;
if (in1 != 0) {
out1 = 0;
atomic_store_explicit((_Atomic int*)ptr1, in1, memory_order_release);
} else {
atomic_store_explicit((_Atomic int*)ptr1, in1, memory_order_relaxed);
}
return out1;
}
AArch64 gcc6.3 -O3输出(Godbolt compiler explorer):
foo_ifelse:
cbnz w0, .L9 @ compare-and-branch-non-zero
str wzr, [x1] @ plain (relaxed) store
mov w0, 123
ret
.L9:
stlr w0, [x1] @ release-store
mov w0, 0
ret
您可以 将order
参数设置为变量,以简化源代码,但是gcc用它做的很不好。 (叮当声将其变成分支)。即使在这种情况下只有两个选项被放宽并释放,GCC仍将其增强为seq_cst。
// don't do this, gcc just strengthen variable-order to seq_cst
int foo_variable_order(int in1, int *ptr1) {
int out1=123;
memory_order order = memory_order_relaxed;
if (in1 != 0) {
out1 = 0;
order = memory_order_release;
}
// SLOW AND INEFFICIENT with gcc
// but clang distributes it over the branch
atomic_store_explicit((_Atomic int*)ptr1, in1, order);
return out1;
}
非恒定order
要求在asm中分支,或增强到最大程度。
我们真的可以看到过度增强x86的效果,其中gcc为此使用mfence
,而其他人仅使用普通的mov
(在x86 asm中具有释放语义)。同样在ARM32 gcc输出中,我们在存储区之前的dmb
和存储区之后的看到seq-cst而不是仅仅发布。
@ gcc6.3 -Os -mcpu=cortex-m4 -mthumb
foo_variable_order:
dmb ish
str r0, [r1]
dmb ish @ barrier after for seq-cst
cmp r0, #0
ite eq @ branchless out1 = in1 ? 0 : 123
moveq r0, #123
movne r0, #0
bx lr