C语言提供了一些关键字,用于对变量的存储方式进行控制;主要包含register、volatile等。对于函数中的局部变量,如果没有强制指定要求,可能直接优化成寄存器访问;不一定在栈中,具体取决于变量的长度大小和编译器的优化策略。
对于局部变量,优化成寄存器访问,速度会比栈中的变量快很多。这是因为栈中的变量需要通过内存访问,涉及到Cache机制、内存寻址等;而内核中的寄存器可以直接存取,访问速度要快很多。不过优化后并非没有弊端,在调试时优化成寄存器访问的局部变量,很多无法直接通过调试窗口查看。
对于变量的地址,这就涉及到对变量的存储的更精细的控制,包括register和volatile关键字。
register关键字可以要求编译器将局部变量分配到寄存器中,从而提高访问速度;但是不能保证变量一定分配到寄存器中,内部寄存器的数量是有限的(如Cortex-M结构,内部数值寄存器对应的就是r0-r12),对于比较大的结构体变量、数组等,编译器仍然会选择栈中的存储方式。
volatile关键字则正好相反,要求编译器必须访问实际的内存地址,不能优化成内部寄存器访问(这里的内部寄存器指的是Cortex-M结构中的r0-r12数据寄存器,而并非功能性寄存器,如UART配置寄存器)。关于volatile关键字,可以用于如下场景。
并行设备的硬件寄存器(如:模块功能寄存器)。一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)。多线程应用中被几个任务共享的变量
volatile仅保证每次访问变量的最新值,阻止对地址访问的优化;并不能保证其原子性,在多线程环境下,也不能代替锁机制。可以使用C11的_Atomic关键字来实现原子操作。精通volatile的运用,在嵌入式底层中十分重要,也是嵌入式C从业者的基本要求之一。
关于volatile、register、__Atomic关键字,具体示例如下所示。
#include int main(int argc, char *argv[]) { int a1 = 1; // 局部变量,由编译器决定存储方式 register int a2 = 2; // 局部变量,要求编译器将变量分配到寄存器中(对于小的变量一般会优化成寄存器访问) volatile int a3 = 3; // 局部变量,要求每次访问都从内存中读取 _Atomic int a4 = 4; // 局部变量,支持原子操作(底层一般依赖volatile实现) a1 += 1; a2 += 1; a3 += 1; a4 += 1; printf("a1: %d\n", a1); printf("a2: %d\n", a2); printf("a3: %d\n", a3); printf("a4: %d\n", a4); return 0; }具体的输出结果如下所示。
运行结果表明,对于局部变量,编译器一般会优化成寄存器访问,对于volatile关键字,每次访问都从内存中读取。