可能被覆盖的值的内联汇编约束

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: 我看到的一个错误优化是编译器无法将 dmbstr,在条件 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