为什么在C语言中需要使用volatile
?它有什么用途?它会做什么?
为什么在C语言中需要使用volatile
?它有什么用途?它会做什么?
volatile
告诉编译器不要对与 volatile
变量有关的任何内容进行优化。
使用它至少有三个常见的原因,都涉及变量的值可以在没有可见代码的情况下发生变化的情况:
假设你有一个映射到RAM的小硬件,并且有两个地址:一个命令端口和一个数据端口:
typedef struct
{
int command;
int data;
int isBusy;
} MyHardwareGadget;
void SendCommand(MyHardwareGadget* gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isBusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
void SendCommand(volatile MyHardwareGadget* gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isBusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
volatile
在 C 语言中的作用是防止自动缓存变量的值。它会告诉编译器不要缓存这个变量的值,因此每次遇到该volatile
变量时,它都会从主内存中获取最新值的代码。使用这种机制是因为任何时候这个值都可能被操作系统或任何中断修改。因此,使用volatile
可以帮助我们每次都获取最新值。
volatile
的目的是让编译器在进行优化的同时,仍然允许程序员实现不进行这种优化时的语义。标准的作者们希望质量优秀的实现能够支持针对它们的目标平台和应用领域所需的任何语义,并且并不希望编译器的编写者寻求提供符合标准但是不完全愚蠢的最低质量语义(请注意标准的作者在理论基础中明确承认……)。 - supercatvolatile
的另一个用途是信号处理程序。如果您有以下代码:
int quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
编译器可以注意到循环体未触及到 quit
变量并将循环转换为 while (true)
循环。即使 quit
变量在 SIGINT
和 SIGTERM
信号处理程序中设置; 编译器也无法知道。quit
变量被声明为 volatile
,编译器就会被强制每次都加载它,因为它可以在其他地方被修改。这正是您在此情况下想要的。volatile
或其他标记的情况下,它将假定一旦变量进入循环,循环外部不会对其进行修改,即使它是全局变量。 - CesarBgcc -O3 -S
编译extern int global; void fn(void) { while (global != 0) { } }
,并查看生成的汇编文件,在我的机器上它会执行movl global(%rip), %eax
; testl %eax, %eax
; je .L1
; .L4: jmp .L4
,即如果全局变量不为零,则进入无限循环。然后尝试添加volatile
并查看差异。 - CesarBvolatile
关键字告诉编译器,你的变量可能会被其他方式改变,而不仅仅是访问它的代码。例如,它可能是一个 I/O 映射的内存位置。如果在这种情况下没有指定此关键字,则某些变量访问可能会被优化,例如,它的内容可以保存在寄存器中,并且不再读取内存位置。
还请参阅Scott Meyers和Andrei Alexandrescu的文章“C++ and the Perils of Double-Checked Locking”:
因此,在处理某些内存位置(例如内存映射端口或由中断服务例程引用的内存)时,必须暂停一些优化。volatile用于指定对这些位置的特殊处理,具体包括:(1)volatile变量的内容是“不稳定的”(可能通过编译器未知的方式发生变化),(2)对volatile数据的所有写入都是“可观察的”,因此它们必须被严格执行,以及(3)对volatile数据的所有操作按照它们在源代码中出现的顺序执行。前两条规则确保正确的读取和写入。最后一条规则允许实现混合输入和输出的I/O协议。这大致是C和C++的volatile所保证的。
volatile
不能保证原子性。 - too honest for this site我的简单解释如下:
在某些情况下,基于逻辑或代码,编译器会对其认为不会改变的变量进行优化。 volatile
关键字可以防止变量被优化。
例如:
bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
// execute logic for the scenario where the USB isn't connected
}
从上面的代码中,编译器可能认为usb_interface_flag
被定义为0,并且在while循环中它将永远是0。经过优化后,编译器将始终将其视为while(true)
,导致无限循环。
为避免这种情况,我们将标志声明为volatile,告诉编译器该值可能会被外部接口或程序的其他模块更改,即请不要对其进行优化。这就是volatile的用例。
volatile 的一个边缘用途是以下内容。假设你想要计算函数 f
的数值导数:
double der_f(double x)
{
static const double h = 1e-3;
return (f(x + h) - f(x)) / h;
}
问题在于由于舍入误差,x+h-x
通常不等于h
。想一想:当你减去非常接近的数字时,你会失去很多有效数字,这可能破坏导数的计算(比如1.00001-1)。一个可能的解决方法是:double der_f2(double x)
{
static const double h = 1e-3;
double hh = x + h - x;
return (f(x + hh) - f(x)) / hh;
}
但是,根据你的平台和编译器开关的不同,那个函数的第二行可能会被一个高度优化的编译器覆盖。因此你应该写成
volatile double hh = x + h;
hh -= x;
为了强制编译器读取包含hh的内存位置,放弃可能出现的优化机会。
h
或 hh
有什么区别?当计算 hh
时,最后一个公式与第一个公式没有区别。也许应该是 (f(x+h) - f(x))/hh
? - Sergey Zhukovh
和hh
之间的区别在于,hh
通过操作x + h - x
被截断为某个负二次幂。在这种情况下,x + hh
和x
恰好相差hh
。您也可以采用您的公式,它将给出相同的结果,因为x + h
和x + hh
是相等的(这里重要的是分母)。 - Alexandre C.x1=x+h; d = (f(x1)-f(x))/(x1-x)
,而不使用“volatile”。 - Sergey Zhukovvolatile
可以用于强制内存读取或写入为特定大小。例如,可能存在仅支持 8 位或 16 位的内存,而错误类型的读取或写入将产生不良影响。如果没有 volatile
,编译器可以自由地将四个 8 位写入组合成一个 32 位写入,即使内存不支持这种写入方式。甚至有些情况下,for
循环会被更改为 memset
,试图向仅支持 16 位的内存写入字节,但使用 volatile
后这种情况就消失了。 - Dwedit我来举另一个需要使用volatile关键字的场景。
假设你为了更快的I/O操作,将一个文件进行了内存映射,并且这个文件可能在后台发生变化(例如该文件不在您的本地硬盘上,而是由另一台计算机通过网络提供)。
如果你通过指向非volatile对象的指针(在源代码级别)访问内存映射文件的数据,那么编译器生成的代码可以在你没有意识到的情况下多次获取相同的数据。
如果这些数据恰好发生了变化,你的程序可能会使用两个或多个不同版本的数据,并进入不一致状态。这不仅会导致程序逻辑上的错误行为,还可能导致安全漏洞,尤其是当它处理来自不受信任的文件或位置的文件时。
如果你关心安全(应该的),这是一个重要的情况需要考虑。
volatile
抱有太高的期望。举个例子,看看Nils Pipenbrinck的高票答案中的例子。volatile
。 volatile
只用于:
防止编译器进行通常有用和可取的优化。它与线程安全、原子访问甚至内存顺序无关。 void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
{
// Wait while the gadget is busy:
while (gadget->isbusy)
{
// Do nothing here.
}
// Set data first:
gadget->data = data;
// Writing the command starts the action:
gadget->command = command;
}
gadget->data = data
在 gadget->command = command
之前只有在编译代码中才能保证。volatile
的使用是为了防止编译器进行通常有用和理想的优化。按照所写的方式,它听起来像是 volatile
没有任何原因地降低了性能。至于是否足够,这将取决于程序员可能比编译器更了解系统的其他方面。另一方面,如果处理器保证写入特定地址的指令将刷新 CPU 缓存,但编译器没有提供刷新寄存器缓存变量的方法,CPU 就不知道,那么刷新缓存就是无用的。 - supercat