可能被覆盖的值的内联汇编约束
inline assembly constraint for value that might be overwritten
我正在调试一些汇编代码,在阅读了一些文档之后,我不确定我是否 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
约束被解释为 'this value is always set',告诉优化器任何先前的值都是无关紧要的。当然,我不确定如何为 可能 改变的东西编写约束...
我也很关心 ptr1
。指针本身并没有实际设置,但它指向的是。我想知道这是否应该有一个读取约束,并想知道是否有适当的方法来设置这个约束。
请注意,我在多个编译器(gcc 和 clang,以及每个编译器的各种版本)上使用此代码,因此我想避免对特定优化器的任何假设。
没错,"=r"
表示只写。寄存器在输入时失效。编译器不会费心在 asm 之前将任何特定内容放入所选寄存器中,因为它将被覆盖。编译器将优化,就像您在内联 asm 之外编写 out1 = asm_result;
。
"+r"
是一个 input/output 操作数。如果它可能被修改,你需要编译器假设它一直都是。
查看编译器为函数生成的 asm,例如在 Godbolt 编译器资源管理器上。 (https://godbolt.org/)。您可以看到编译器围绕您的内联 asm 生成了哪些代码,包括在内联到另一个函数之后。
I'm also concerned with ptr1
. The pointer itself is not actually set, but what it points to is.
是的,你的担心是对的。 "+r"(ptr1)
告诉编译器指针值已修改,但 not 是否暗示指向的值已修改。 "memory"
clobber 是一种繁重的方法,或者正如 Jester 所说,您应该只使用 "=m"(*ptr1)
约束来让编译器选择寻址模式,并告诉它指向的内存无条件写入。
或者更好,https://gcc.gnu.org/wiki/DontUseInlineAsm
如果没有前面的 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
release store (even though it's a sequential-release, not a weaker plain release). dmb ish
is a full memory barrier. Also, 32-bit code for ARMv8 can use stl
.
请注意,完整的 dmb
将订购 other 以后的商店 wrt。较早的商店,因此这在 AArch64(或 32 位可用的 ARMv8 指令)上并不完全等同,其中编译器生成的代码不使用 dmb
.
此版本编译为适用于所有体系结构的相当不错的 asm: 我看到的一个错误优化是编译器无法将 dmb
与 str
,在条件 dmb
后留下一个普通的 str
。 (对于必须使用 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 对此做得很糟糕。 (clang 把它变回一个分支)。 GCC 将它加强到 seq_cst,即使在这种情况下仅有的 2 个选项是 relaxed 和 release。
// 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
,但对于其他(在 x86 asm 中具有释放语义)仅使用 mov
。同样在 ARM32 gcc 输出中,我们看到 之前的 dmb
和存储之后的 ,对于 seq-cst 而不仅仅是 release。
@ 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
我正在调试一些汇编代码,在阅读了一些文档之后,我不确定我是否 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
约束被解释为 'this value is always set',告诉优化器任何先前的值都是无关紧要的。当然,我不确定如何为 可能 改变的东西编写约束...
我也很关心 ptr1
。指针本身并没有实际设置,但它指向的是。我想知道这是否应该有一个读取约束,并想知道是否有适当的方法来设置这个约束。
请注意,我在多个编译器(gcc 和 clang,以及每个编译器的各种版本)上使用此代码,因此我想避免对特定优化器的任何假设。
没错,"=r"
表示只写。寄存器在输入时失效。编译器不会费心在 asm 之前将任何特定内容放入所选寄存器中,因为它将被覆盖。编译器将优化,就像您在内联 asm 之外编写 out1 = asm_result;
。
"+r"
是一个 input/output 操作数。如果它可能被修改,你需要编译器假设它一直都是。
查看编译器为函数生成的 asm,例如在 Godbolt 编译器资源管理器上。 (https://godbolt.org/)。您可以看到编译器围绕您的内联 asm 生成了哪些代码,包括在内联到另一个函数之后。
I'm also concerned with
ptr1
. The pointer itself is not actually set, but what it points to is.
是的,你的担心是对的。 "+r"(ptr1)
告诉编译器指针值已修改,但 not 是否暗示指向的值已修改。 "memory"
clobber 是一种繁重的方法,或者正如 Jester 所说,您应该只使用 "=m"(*ptr1)
约束来让编译器选择寻址模式,并告诉它指向的内存无条件写入。
或者更好,https://gcc.gnu.org/wiki/DontUseInlineAsm
如果没有前面的 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
release store (even though it's a sequential-release, not a weaker plain release). dmb ish
is a full memory barrier. Also, 32-bit code for ARMv8 can use stl
.
请注意,完整的 dmb
将订购 other 以后的商店 wrt。较早的商店,因此这在 AArch64(或 32 位可用的 ARMv8 指令)上并不完全等同,其中编译器生成的代码不使用 dmb
.
此版本编译为适用于所有体系结构的相当不错的 asm: 我看到的一个错误优化是编译器无法将 dmb
与 str
,在条件 dmb
后留下一个普通的 str
。 (对于必须使用 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 对此做得很糟糕。 (clang 把它变回一个分支)。 GCC 将它加强到 seq_cst,即使在这种情况下仅有的 2 个选项是 relaxed 和 release。
// 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
,但对于其他(在 x86 asm 中具有释放语义)仅使用 mov
。同样在 ARM32 gcc 输出中,我们看到 之前的 dmb
和存储之后的 ,对于 seq-cst 而不仅仅是 release。
@ 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