避免易失性位域赋值表达式多次读取或写入内存

Avoid volatile bit-field assignment expression reading or writing memory several times

我想使用易失性位域结构来设置硬件寄存器,如下面的代码

union foo {
    uint32_t value;
    struct {
        uint32_t x : 1;
        uint32_t y : 3;
        uint32_t z : 28;
    };
};
union foo f = {0};
int main()
{
    volatile union foo *f_ptr = &f;
    //union foo tmp;
    *f_ptr =  (union foo) {
        .x = 1,
        .y = 7,
        .z = 10,
    };
    //*f_ptr = tmp;
    return 0;
}

但是,编译器会多次向STRLDR HW 注册。 写寄存器的时候会立即触发硬件工作,这是很可怕的事情。

main:
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 0, uses_anonymous_args = 0
    @ link register save eliminated.
    movw    r3, #:lower16:.LANCHOR0
    movs    r0, #0
    movt    r3, #:upper16:.LANCHOR0
    ldr r2, [r3]
    orr r2, r2, #1
    str r2, [r3]
    ldr r2, [r3]
    orr r2, r2, #14
    str r2, [r3]
    ldr r2, [r3]
    and r2, r2, #15
    orr r2, r2, #160
    str r2, [r3]
    bx  lr
    .size   main, .-main
    .global f
    .bss
    .align  2

我的 gcc 版本是:arm-linux-gnueabi-gcc (Linaro GCC 4.9-2017.01) 4.9.4 并使用 -O2 优化构建


我试过使用局部变量来解决这个问题

union foo {
    uint32_t value;
    struct {
        uint32_t x : 1;
        uint32_t y : 3;
        uint32_t z : 28;
    };
};
union foo f = {0};
int main()
{
    volatile union foo *f_ptr = &f;
    union foo tmp;
    tmp =  (union foo) {
        .x = 1,
        .y = 7,
        .z = 10,
    };
    *f_ptr = tmp;
    return 0;
}

嗯,不会STR到HW注册几次

main:
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 0, uses_anonymous_args = 0
    @ link register save eliminated.
    movs    r1, #10
    movs    r2, #15
    movw    r3, #:lower16:.LANCHOR0
    bfi r2, r1, #4, #28
    movt    r3, #:upper16:.LANCHOR0
    movs    r0, #0
    str r2, [r3]
    bx  lr
    .size   main, .-main
    .global f
    .bss
    .align  2

考虑到嵌入式系统二进制大小的限制,我认为使用局部变量仍然不是一个好主意。

有什么办法可以不使用局部变量来解决这个问题吗?


您的程序中似乎不需要位域:使用 uint16_t 类型应该会使它更简单并生成更好的代码:

#include <stdint.h>

union foo {
    uint32_t value;
    struct {
        uint16_t x;
        uint16_t y;
    };
};
extern union foo f;

int main() {
    volatile union foo *f_ptr = &f;
    *f_ptr = (union foo) {
        .x = 10,
        .y = 20,
    };
    return 0;
}

arm gcc 4.6.4 linux 生成的代码,由 Godbolt Compiler Explorer 生成:

main:
        ldr     r3, .L2
        mov     r0, #0
        mov     r2, #10
        str     r0, [r3, #0]
        strh    r2, [r3, #0]    @ movhi
        mov     r2, #20
        strh    r2, [r3, #2]    @ movhi
        bx      lr
.L2:
        .word   f

代码简单得多,但仍然对 32 位值执行冗余存储:str r0, [r3, #0] 一次性存储并集时。

对此进行调查,我尝试了不同的方法并得到了令人惊讶的结果:使用结构或联合分配生成的代码可能不适合内存映射硬件寄存器,按元素存储字段似乎需要生成正确的代码:

#include <stdint.h>

union foo {
    uint32_t value;
    struct {
        uint16_t x;
        uint16_t y;
    };
};
extern union foo f;

void store_1(void) {
    volatile union foo *f_ptr = &f;
    *f_ptr = (union foo) {
        .x = 10,
        .y = 20,
    };
}
void store_2(void) {
    volatile union foo *f_ptr = &f;
    union foo bar = { .x = 10, .y = 20, };
    *f_ptr = bar;
}
void store_3(void) {
    volatile union foo *f_ptr = &f;
    f_ptr->x = 10;
    f_ptr->y = 20;
}
int main() {
    return 0;
}

此外,删除 uint32_t value; 会为结构赋值版本生成对 memcpy 的调用。

代码由arm gcc 4.6.4 linux生成:

store_1:
        ldr     r3, .L2
        mov     r2, #0
        str     r2, [r3, #0]
        mov     r2, #10
        strh    r2, [r3, #0]    @ movhi
        mov     r2, #20
        strh    r2, [r3, #2]    @ movhi
        bx      lr
.L2:
        .word   f
store_2:
        ldr     r3, .L5
        ldr     r2, .L5+4
        str     r2, [r3, #0]
        bx      lr
.L5:
        .word   f
        .word   1310730
store_3:
        ldr     r3, .L8
        mov     r2, #10
        strh    r2, [r3, #0]    @ movhi
        mov     r2, #20
        strh    r2, [r3, #2]    @ movhi
        bx      lr
.L8:
        .word   f
main:
        mov     r0, #0
        bx      lr

进一步调查似乎 link 使用 volatile union foo *f_ptr = &f; 而不是将工会成员标记为 volatile:

的问题
#include <stdint.h>

union foo {
    uint32_t value;
    struct {
        volatile uint16_t x;
        volatile uint16_t y;
    };
};
extern union foo f;

void store_1(void) {
    union foo *f_ptr = &f;
    *f_ptr = (union foo) {
        .x = 10,
        .y = 20,
    };
    *f_ptr = (union foo) {
        .x = 10,
        .y = 20,
    };
}
void store_2(void) {
    union foo *f_ptr = &f;
    union foo bar = { .x = 10, .y = 20, };
    *f_ptr = bar;
    *f_ptr = bar;
}
void store_3(void) {
    union foo *f_ptr = &f;
    f_ptr->x = 10;
    f_ptr->y = 20;
    f_ptr->x = 10;
    f_ptr->y = 20;
}

Code 生成:

store_1:
        ldr     r3, .L2
        mov     r1, #10
        mov     r2, #20
        strh    r1, [r3, #0]    @ movhi
        strh    r2, [r3, #2]    @ movhi
        strh    r1, [r3, #0]    @ movhi
        strh    r2, [r3, #2]    @ movhi
        bx      lr
.L2:
        .word   f
store_2:
        ldr     r3, .L5
        ldr     r2, .L5+4
        str     r2, [r3, #0]
        bx      lr
.L5:
        .word   f
        .word   1310730
store_3:
        ldr     r3, .L8
        mov     r1, #10
        mov     r2, #20
        strh    r1, [r3, #0]    @ movhi
        strh    r2, [r3, #2]    @ movhi
        strh    r1, [r3, #0]    @ movhi
        strh    r2, [r3, #2]    @ movhi
        bx      lr
.L8:
        .word   f

如您所见,即使将 value 成员限定为 volatile,分配联合也不会在 store_2 中生成适当的代码。

使用 C99 复合文字似乎在 store_1 中可以正常工作,当字段限定为 volatile.

时生成冗余存储

但我建议像 store_3 中那样显式分配字段,也使分配顺序明确。相反,如果您想生成单个 32 位存储,假设它适合您的硬件,Aki Suihkonen 建议 .

最初的问题是编译器如何生成将复合文字分配给结构和联合的代码的副作用:它首先将目标初始化为所有位零,然后显式存储复合文字中指定的成员。除非目标是 volatile qualified`,否则将消除冗余存储。我不认为这种行为是 C 标准强制要求的,因此它很可能是特定于编译器的。

你可以用

强制一次性写入聚合联合
f_ptr->value = (union foo) {
    .x = 10,
    .y = 20,
}.value;

// produced asm
mov     r1, #10
orr     r1, r1, #1310720
str     r1, [r0]
bx      lr

我认为这是 GCC 中的错误。根据下面的讨论,您可以考虑使用:

f_ptr->value =  (union foo) {
        .x = 1,
        .y = 7,
        .z = 10,
    } .value;

根据 C 标准,当原始 C 代码名义上不访问对象时,编译器为程序生成的代码可能不会访问易失性对象。代码 *f_ptr = (union foo) { .x = 1, .y = 7, .z = 10, }; 是对 *f_ptr 的单一赋值。所以我们希望这会生成一个存储到 *f_ptr;生成两个商店是违反标准要求的。

我们可以考虑这样一种解释,即 GCC 将聚合(联合 and/or 中的结构)视为多个对象,每个对象都是易失性的,而不是一个聚合的易失性对象。1 但是,如果是这样,那么它应该为这些部分生成单独的 16 位 strh 指令(根据原始示例代码,它有 16 位部分),而不是我们看到的 32 位 str 指令。

虽然使用局部变量似乎可以解决这个问题,但我不会依赖它,因为上面复合文字的赋值在语义上是等价的,所以 GCC 为一个序列生成损坏的汇编代码的原因代码而不是其他不清楚。在不同的情况下(例如函数中的附加或修改代码或可能影响优化的其他变体),GCC 也可能会使用局部变量生成损坏的代码。

我会做的是避免对易失性对象使用聚合。据推测,硬件寄存器在物理上更像是一个 32 位无符号整数,而不是位域结构(尽管在语义上它是用位域定义的)。所以我会将寄存器定义为 volatile uint32_t 并在为其赋值时使用该类型。这些值可以用位移位或带位域的结构或您喜欢的任何其他方法来准备。

没有必要避免使用局部变量,因为优化器应该有效地消除它们。但是,如果您既不希望更改寄存器定义也不希望使用局部变量,另一种方法是我打开的代码:

f_ptr->value =  (union foo) {
        .x = 1,
        .y = 7,
        .z = 10,
    } .value;

准备要存储的值,然后使用联合的 uint32_t 成员而不是使用整个联合来分配它,并且 testing with ARM GCC 4.6.4 on Compiler Explorer(我在 Compiler Explorer 上找到的最接近的匹配项到你正在使用的)建议它用最少的代码生成一个商店:

main:
        ldr     r3, .L2
        mov     r2, #175
        str     r2, [r3, #0]
        mov     r0, #0
        bx      lr
.L2:
        .word   .LANCHOR0
.LANCHOR0 = . + 0
f:

脚注

1 我也认为这是一个错误,因为 C 标准没有规定在联合或结构声明上应用 volatile 限定符作为应用对成员而不是整个集合。对于数组,它确实表示限定符适用于元素,而不是整个数组 (C 2018 6.7.3 10)。对于联合或结构,它没有这样的措辞。