Answers:
易失性告诉编译器不要优化与易失性变量有关的任何事情。
至少有三个使用它的常见原因,所有这些情况都涉及变量的值可以更改而无需可见代码采取行动的情况。当另一个正在运行的线程也使用该变量时;或存在可能更改变量值的信号处理程序时。
假设您有一小部分硬件映射到某处的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;
}
看起来很简单,但可能会失败,因为编译器可以自由更改写入数据和命令的顺序。这将导致我们的小工具发布具有先前数据值的命令。还要看看忙循环时的等待。那将被优化。编译器将尝试变得更聪明,只读取一次isbusy的值,然后进入无限循环。那不是你想要的。
解决此问题的方法是将指针小工具声明为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;
}
volatile
实际上,C语言中的C语言是为了不自动缓存变量的值而存在的。它会告诉编译器不要缓存该变量的值。因此,它将volatile
在每次遇到主变量时生成代码以从主内存中获取给定变量的值。之所以使用此机制,是因为该值可以随时通过OS或任何中断进行修改。因此,使用volatile
有助于我们每次重新获得价值。
volatile
是使编译器可以优化代码,同时仍然允许程序员获得无需这种优化即可实现的语义。该标准的作者期望,质量实现将支持给定目标平台和应用程序领域有用的任何语义,并且不希望编译器作者寻求提供符合该标准且质量不是100%的最低质量的语义。愚蠢的(请注意,标准的作者在原理上明确承认……
volatile
信号处理程序的另一个用途是。如果您有这样的代码:
int quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
允许编译器注意循环主体不接触quit
变量并将循环转换为while (true)
循环。即使quit
变量被设置为上信号处理程序SIGINT
和SIGTERM
; 编译器没有办法知道。
但是,如果quit
声明了变量volatile
,则编译器每次都必须加载它,因为可以在其他地方对其进行修改。在这种情况下,这正是您想要的。
quit
,则编译器可以将其优化为一个常量循环,并假定quit
在迭代之间没有办法改变。注意:这不一定是实际线程安全编程的良好替代品。
volatile
或其他标记的情况下,即使变量是全局变量,也将假定该变量一旦进入循环,循环外便不会有任何修改。
extern int global; void fn(void) { while (global != 0) { } }
与gcc -O3 -S
和看所产生的汇编文件,我的机器上它movl global(%rip), %eax
; testl %eax, %eax
; je .L1
; .L4: jmp .L4
,即如果全局不为零,则为无限循环。然后尝试添加volatile
并查看差异。
请参阅Andrei Alexandrescu的这篇文章,“ volatile-多线程程序员的最好朋友 ”
该挥发性关键字进行设计,以防止可能使代码在某些异步事件的存在不正确的编译器优化。例如,如果将原始变量声明为 volatile,则不允许编译器将其缓存在寄存器中-这是一种常见的优化方法,如果在多个线程之间共享该变量,那将是灾难性的。因此,通常的规则是,如果必须在多个线程之间共享原始类型的变量,则将这些变量声明为volatile。但是实际上您可以使用此关键字做更多的事情:您可以使用它来捕获不是线程安全的代码,并且可以在编译时执行。本文说明了它是如何完成的。该解决方案涉及一个简单的智能指针,该指针也使序列化关键代码段变得容易。
本文适用于C
和C++
。
另请参阅Scott Meyers和Andrei Alexandrescu 的文章“ C ++和双重检查锁定的风险 ”:
因此,在处理某些内存位置(例如,内存映射端口或ISR引用的内存[中断服务例程])时,必须暂停某些优化。存在volatile来指定对此类位置的特殊处理,具体来说:(1)volatile变量的内容“不稳定”(可以通过编译器未知的方式更改),(2)所有对volatile数据的写入都是“可观察的”,因此它们必须认真执行,并且(3)对易失性数据进行的所有操作均应按其在源代码中出现的顺序执行。前两个规则确保正确的读写。最后一个允许实现混合输入和输出的I / O协议。非正式地,这就是C和C ++的易失性保证。
volatile
不保证原子性。
我的简单解释是:
在某些情况下,基于逻辑或代码,编译器将优化它认为不会改变的变量。的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循环中,它将永远为零。经过优化后,编译器将一直将其视为while(true)
永久循环,从而导致无限循环。
为了避免这种情况,我们将标志声明为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
吗?
h
和之间的区别hh
是hh
通过运算被截断为2的某个负幂x + h - x
。在这种情况下,x + hh
和x
不同的准确hh
。您也可以采用公式,由于x + h
和x + hh
相等,公式将给出相同的结果(分母在这里很重要)。
x1=x+h; d = (f(x1)-f(x))/(x1-x)
呢?无需使用挥发物。
-ffast-math
或等效进行优化。
有两种用途。这些在嵌入式开发中更经常使用。
编译器不会优化使用由volatile关键字定义的变量的函数
易失性用于访问RAM,ROM等中的确切内存位置。这通常用于控制内存映射的设备,访问CPU寄存器并定位特定的内存位置。
请参见带有装配清单的示例。 回复:嵌入式开发中C“易失性”关键字的使用
当您要强制编译器不优化特定的代码序列(例如,编写微基准测试)时,Volatile也很有用。
我将提到另一种情况,其中挥发物很重要。
假设您对文件进行内存映射以获得更快的I / O,并且该文件可以在后台更改(例如,该文件不在本地硬盘上,而是由另一台计算机通过网络提供)。
如果您通过指向非易失性对象的指针(在源代码级别)访问内存映射文件的数据,则编译器生成的代码可以多次获取同一数据,而无需您意识到。
如果该数据发生更改,则您的程序可能会使用两个或多个不同版本的数据,并进入不一致状态。如果程序处理不受信任的文件或来自不受信任位置的文件,则不仅可能导致程序在逻辑上不正确的行为,而且还可能导致程序中的可利用安全漏洞。
如果您确实要关心安全性,那么这是要考虑的重要方案。
易失性意味着存储可能随时更改并且可能会更改,但是某些内容超出了用户程序的控制范围。这意味着,如果您引用变量,则程序应始终检查物理地址(即映射的输入fifo),而不是以缓存的方式使用它。
我认为,您不应期望太多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
仅只有在编译器编译代码的保证。在运行时,关于处理器体系结构,处理器仍可能对数据和命令分配进行重新排序。硬件可能会获得错误的数据(假设小工具已映射到硬件I / O)。在数据和命令分配之间需要内存屏障。
volatile
毫无理由地降低了性能。至于是否足够,这将取决于程序员可能比编译器了解更多的系统其他方面。另一方面,如果处理器保证写入某个地址的指令将刷新CPU高速缓存,但是编译器没有提供刷新CPU所不知道的寄存器高速缓存变量的方法,则刷新高速缓存将是无用的。
在丹尼斯·里奇(Dennis Ritchie)设计的语言中,对任何对象的每次访问(未使用其地址的自动对象除外)都将表现为好像计算了对象的地址,然后在该地址读取或写入了存储。这使得该语言非常强大,但是优化机会却非常有限。
虽然可能可以添加一个限定符来邀请编译器假定一个特定的对象不会以怪异的方式进行更改,但是这种假定将适用于C程序中的绝大多数对象。在这样的假设适合的所有对象上添加限定符是不切实际的。另一方面,某些程序需要使用某些假设无法满足的对象。为解决此问题,标准指出,编译器可能会假设未声明的对象volatile
将不会以其编译器无法控制的方式或合理的编译器无法理解的方式来观察或更改其值。
由于各种平台可能具有不同的方式来观察或修改编译器无法控制的对象,因此适合这些平台的高质量编译器应在volatile
语义的精确处理方面有所不同。不幸的是,由于该标准未能建议打算在平台上进行低级编程的高质量编译器应volatile
以能够识别该平台上特定读/写操作的所有及所有相关影响的方式进行处理,因此许多编译器无法做到这一点因此,这种方式将使得以高效的方式处理后台I / O之类的事情变得更加困难,但不会被编译器的“优化”破坏。
可以从已编译的代码外部更改volatile(例如,程序可能将volatile变量映射到内存映射的寄存器。)编译器不会对处理volatile变量的代码进行某些优化-例如,它将不会t将其加载到寄存器中而不将其写入内存。在处理硬件寄存器时,这一点很重要。