4.1 VOLATILE关键字
当你使用一个不能被编译器优化的变量时,需要使用volatile关键字,如果在需要使用 volatile 的地方没有使用它,编译器可能会优化对变量的访问,从而生成非预期的代码,甚至移除程序中原本预期的功能。
4.1.1 volatile 的含义
将变量声明为 volatile,告诉编译器该变量可能在任何时间被当前实现代码之外的代码修改。例如:
- 由操作系统修改
- 由硬件修改
这种声明保证编译器不会基于“变量未被使用或未被修改”的假设对其进行优化。你也可以使用 volatile 来告诉编译器:某个包含内联汇编(inline assembly)的代码块具有输入列表、输出列表和 clobber 列表所不能完全描述的副作用。
4.1.2 何时使用 volatile
当一个变量可能会在其定义作用域之外被修改时,应当使用 volatile 关键字。例如,如果程序在某次计算中使用了一个全局变量,编译器通常会生成指令,将该变量的值加载到寄存器中以执行计算。如果该全局变量随后在另一段计算中再次使用,编译器可能会复用寄存器中已有的值,而不是重新从内存加载。这种复用行为的原因在于:
编译器优化器默认假设非 volatile 变量不会被外部实体修改。但这个假设对于内存映射外设(memory-mapped peripherals)是不成立的 ——外设寄存器的值经常会在程序未知的情况下被硬件修改。具体请参考 4.1.5 “未使用 volatile 关键字导致的无限循环示例”。
另一个例子是:某个变量可能被用来实现 sleep 或定时延迟。如果该变量看起来未被使用,编译器可能会移除这段用于计时延迟的代码,除非该变量被声明为 volatile。
在实践中:
- 当访问内存映射外设时,必须将变量声明为 volatile。即使在 -O0 优化等级下,也不能保证编译器会默认把所有相关变量视为 volatile。
- 中断处理程序中会修改的变量并在外部使用时,应使用 volatile关键字修饰
4.1.3 未使用 volatile 时可能出现的潜在问题
当一个需要声明为 volatile 的变量没有声明为 volatile,编译器会假定该变量的值不会在其定义作用域之外被修改。因此,编译器可能会执行一些不希望发生的优化。这个问题可能以多种形式表现出来,例如:
- 在轮询硬件时,代码可能会卡在循环中
- 优化可能导致实现刻意延迟(timing delay)的代码被移除
4.1.4 强制使用特定指令访问内存
将变量声明为volatile并不能保证编译器在访问该变量时使用某条特定的机器指令。仅仅在变量或指针类型上添加 volatile 修饰符,并不能保证编译器会自动选择该寄存器所要求的访问方式。如果你的代码需要特定访问方式的内存映射位置,那么仅声明为 volatile 是不够的。你还必须使用 __asm__ 内联汇编语句,并在其中显式写入你所需要的加载或存储指令。
4.1.5 未使用 volatile 关键字导致的无限循环示例
下表中的两个示例对比了buffer_full不使用volatile与使用volatile两种写法。
表4.1 buffer 循环的非 volatile和volatile 版本C语言代码
| buffer 循环的非 volatile 版本 | buffer 循环的 volatile 版本 |
|---|---|
两个例程都在循环中递增计数器,直到buffer_full被置为true。buffer_full可能在程序流程之外被异步修改。
左例未将buffer_full声明为volatile,编译器只在首次访问时从内存加载该变量,之后复用寄存器中的值,无法感知异步变化,循环可能永远不会退出;右例将其声明为volatile,编译器每次访问都会重新从内存加载,因此正确。