C99:访问全局变量和别名内存指针时的编译器优化

C99: compiler optimizations when accessing global variables and aliased memory pointers

我正在为嵌入式系统编写 C 代码。在这个系统中,在内存映射中的某个固定地址处有内存映射寄存器,当然还有我的数据段/堆所在的一些RAM。

当我的代码混合访问数据段中的全局变量和访问硬件寄存器时,我发现生成最佳代码时出现问题。这是一个简化的片段:

#include <stdint.h>

uint32_t * const restrict HWREGS = 0x20000;

struct {
    uint32_t a, b;
} Context;

void example(void) {
    Context.a = 123;
    HWREGS[0x1234] = 5;
    Context.b = Context.a;
}

这是在 x86 上生成的代码(另见 godbolt):

example:
        mov     DWORD PTR Context[rip], 123
        mov     DWORD PTR ds:149712, 5
        mov     eax, DWORD PTR Context[rip]
        mov     DWORD PTR Context[rip+4], eax
        ret

如您所见,写入硬件寄存器后,Context.a 在存储到 Context.b 之前从 RAM 重新加载。这没有意义,因为 ContextHWREGS 的内存地址不同。换句话说,HWREGS 指向的内存和 &Context 指向的内存没有别名,但看起来没有办法告诉编译器。

如果我将 HWREGS 定义更改为:

extern uint32_t * const restrict HWREGS;

也就是我把固定的内存地址隐藏给编译器,我得到这个:

example:
        mov     rax, QWORD PTR HWREGS[rip]
        mov     DWORD PTR [rax+18640], 5
        movabs  rax, 528280977531
        mov     QWORD PTR Context[rip], rax
        ret
Context:
        .zero   8

现在对 Context 的两次写入已优化(甚至合并为一次写入),但另一方面,直接内存访问不再发生对硬件寄存器的访问,而是通过指针间接访问。

有没有办法在这里获得最佳代码?我希望 GCC 知道 HWREGS 位于固定的内存地址,同时告诉它它没有别名 Context.

如果你想避免编译器定期从内存区域重新加载值(可能是由于别名),那么最好不要使用全局变量,或者至少 不要使用对全局变量的直接访问变量。 GCC 和 Clang 的全局变量(尤其是在 HWREGS 上)似乎忽略了 register 关键字。在函数参数上使用 restrict 关键字解决了这个问题:

#include <stdint.h>

uint32_t * const HWREGS = 0x20000;

struct Context {
    uint32_t a, b;
} context;

static inline void exampleWithLocals(uint32_t* restrict localRegs, struct Context* restrict localContext) {
    localContext->a = 123;
    localRegs[0x1234] = 5;
    localContext->b = localContext->a;
}

void example() {
    exampleWithLocals(HWREGS, &context);
}

这是结果(另见 godbolt):

example:
        movabs  rax, 528280977531
        mov     DWORD PTR ds:149712, 5
        mov     QWORD PTR context[rip], rax
        ret
context:
        .zero   8

请注意,严格的别名规则在这种情况下没有帮助,因为 read/written variables/fields 的类型始终是 uint32_t

除此之外,根据它的名字,变量HWREGS看起来像一个硬件寄存器。请注意,它应该放在 volatile 中,这样编译器就不会将它保留在寄存器中,也不会执行任何类似的优化(比如假设如果代码不更改它,指向的值将保持不变)。