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 重新加载。这没有意义,因为 Context
与 HWREGS
的内存地址不同。换句话说,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
中,这样编译器就不会将它保留在寄存器中,也不会执行任何类似的优化(比如假设如果代码不更改它,指向的值将保持不变)。
我正在为嵌入式系统编写 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 重新加载。这没有意义,因为 Context
与 HWREGS
的内存地址不同。换句话说,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
中,这样编译器就不会将它保留在寄存器中,也不会执行任何类似的优化(比如假设如果代码不更改它,指向的值将保持不变)。