为什么 C 中需要 volatile?
为什么 C 中需要易失性
? 它是干什么用的? 它会做什么?
Why is volatile
needed in C? What is it used for? What will it do?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
为什么 C 中需要易失性
? 它是干什么用的? 它会做什么?
Why is volatile
needed in C? What is it used for? What will it do?
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
接受
或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
发布评论
评论(19)
易失性
告诉编译器不要优化与易失性
变量有关的任何内容。使用它至少有三个常见原因,所有这些都涉及变量的值无需可见代码的操作即可更改
假设您有一小块硬件映射到 RAM 的某个位置,并且有两个地址:一个命令端口和一个数据端口:
现在您想要发送一些命令:
看起来很简单,但它可能会失败,因为编译器可以自由地更改写入数据和命令的顺序。 这将导致我们的小工具使用先前的数据值发出命令。 另请查看 wait while busy 循环。 那个将被优化掉。 编译器会尝试变得聪明,只读取一次 isBusy 的值,然后进入无限循环。 那不是你想要的。
解决这个问题的方法是将指针
gadget
声明为易失性
。 这样编译器就被迫执行您编写的操作。 它不能删除内存分配,不能在寄存器中缓存变量,也不能更改分配的顺序这是正确的版本:
volatile
tells the compiler not to optimize anything that has to do with thevolatile
variable.There are at least three common reasons to use it, all involving situations where the value of the variable can change without action from the visible code:
Let's say you have a little piece of hardware that is mapped into RAM somewhere and that has two addresses: a command port and a data port:
Now you want to send some command:
Looks easy, but it can fail because the compiler is free to change the order in which data and commands are written. This would cause our little gadget to issue commands with the previous data-value. Also take a look at the wait while busy loop. That one will be optimized out. The compiler will try to be clever, read the value of
isBusy
just once and then go into an infinite loop. That's not what you want.The way to get around this is to declare the pointer
gadget
asvolatile
. This way the compiler is forced to do what you wrote. It can't remove the memory assignments, it can't cache variables in registers and it can't change the order of assignments eitherThis is the correct version:
C中的
volatile
实际上是为了不自动缓存变量的值而存在的。 它会告诉编译器不要缓存该变量的值。 因此,每次遇到给定的易失性
变量时,它都会生成代码从主内存中获取该变量的值。 使用此机制是因为操作系统或任何中断都可以随时修改该值。 因此,使用易失性
将帮助我们每次都重新访问该值。volatile
in C actually came into existence for the purpose of not caching the values of the variable automatically. It will tell the compiler not to cache the value of this variable. So it will generate code to take the value of the givenvolatile
variable from the main memory every time it encounters it. This mechanism is used because at any time the value can be modified by the OS or any interrupt. So usingvolatile
will help us accessing the value afresh every time.易失性的另一个用途是信号处理程序。 如果您有这样的代码:
编译器可以注意到循环体没有触及
quit
变量,并将循环转换为while (true)
循环。 即使在SIGINT
和SIGTERM
的信号处理程序上设置了quit
变量; 编译器无法知道这一点。但是,如果
quit
变量被声明为volatile
,则编译器每次都会被迫加载它,因为它可以在其他地方修改。 在这种情况下,这正是您想要的。Another use for
volatile
is signal handlers. If you have code like this:The compiler is allowed to notice the loop body does not touch the
quit
variable and convert the loop to awhile (true)
loop. Even if thequit
variable is set on the signal handler forSIGINT
andSIGTERM
; the compiler has no way to know that.However, if the
quit
variable is declaredvolatile
, the compiler is forced to load it every time, because it can be modified elsewhere. This is exactly what you want in this situation.易失性
告诉编译器您的变量可能会通过其他方式更改,而不是访问它的代码。 例如,它可以是 I/O 映射的内存位置。 如果在这种情况下没有指定,则可以优化某些变量访问,例如,其内容可以保存在寄存器中,并且不会再次读回存储器位置。volatile
tells the compiler that your variable may be changed by other means, than the code that is accessing it. e.g., it may be a I/O-mapped memory location. If this is not specified in such cases, some variable accesses can be optimised, e.g., its contents can be held in a register, and the memory location not read back in again.请参阅 Andrei Alexandrescu 撰写的这篇文章,“易失性 - 多线程程序员的最好朋友”
本文适用于 C 和 C++。
另请参阅 Scott Meyers 的文章“C++ 和双重检查锁定的危险”和安德烈·亚历山德雷斯库:
See this article by Andrei Alexandrescu, "volatile - Multithreaded Programmer's Best Friend"
The article applies to both C and C++.
Also see the article "C++ and the Perils of Double-Checked Locking" by Scott Meyers and Andrei Alexandrescu:
我简单的解释是:
在某些场景下,编译器会根据逻辑或代码,对它认为不会改变的变量进行优化。
volatile
关键字可防止变量被优化。例如:
从上面的代码来看,编译器可能认为
usb_interface_flag
被定义为0,并且在while循环中它将永远为零。 优化后,编译器会一直将其视为while(true)
,从而导致无限循环。为了避免这种情况,我们将标志声明为易失性,我们告诉编译器该值可能会被外部接口或程序的其他模块更改,即请不要优化它。 这就是 volatile 的用例。
My simple explanation is:
In some scenarios, based on the logic or code, the compiler will do optimisation of variables which it thinks do not change. The
volatile
keyword prevents a variable being optimised.For example:
From the above code, the compiler may think
usb_interface_flag
is defined as 0, and that in the while loop it will be zero forever. After optimisation, the compiler will treat it aswhile(true)
all the time, resulting in an infinite loop.To avoid these kinds of scenarios, we declare the flag as volatile, we are telling to compiler that this value may be changed by an external interface or other module of program, i.e., please don't optimise it. That's the use case for volatile.
挥发性的边际用途如下。 假设您要计算函数
f
的数值导数:问题是由于舍入,
x+hx
通常不等于h
错误。 想一想:当你减去非常接近的数字时,你会丢失很多有效数字,这可能会破坏导数的计算(想想 1.00001 - 1)。 一种可能的解决方法是,但根据您的平台和编译器开关,该函数的第二行可能会被积极优化的编译器删除。 因此,您改为
强制编译器读取包含 hh 的内存位置,从而放弃最终的优化机会。
A marginal use for volatile is the following. Say you want to compute the numerical derivative of a function
f
:The problem is that
x+h-x
is generally not equal toh
due to roundoff errors. Think about it : when you substract very close numbers, you lose a lot of significant digits which can ruin the computation of the derivative (think 1.00001 - 1). A possible workaround could bebut depending on your platform and compiler switches, the second line of that function may be wiped out by a aggressively optimizing compiler. So you write instead
to force the compiler to read the memory location containing hh, forfeiting an eventual optimization opportunity.
我会提到另一个场景,其中挥发物很重要。
假设您对一个文件进行内存映射以实现更快的 I/O,并且该文件可以在幕后更改(例如,该文件不在您的本地硬盘驱动器上,而是由另一台计算机通过网络提供服务)。
如果您通过指向非易失性对象的指针(在源代码级别)访问内存映射文件的数据,则编译器生成的代码可以在您不知情的情况下多次获取相同的数据。
如果该数据碰巧发生更改,您的程序可能会使用两个或多个不同版本的数据并进入不一致的状态。 如果程序处理不受信任的文件或来自不受信任位置的文件,这不仅会导致程序在逻辑上不正确的行为,还会导致程序中存在可利用的安全漏洞。
如果您关心安全性(而且您应该关心安全性),那么这是一个需要考虑的重要场景。
I'll mention another scenario where volatiles are important.
Suppose you memory-map a file for faster I/O and that file can change behind the scenes (e.g. the file is not on your local hard drive, but is instead served over the network by another computer).
If you access the memory-mapped file's data through pointers to non-volatile objects (at the source code level), then the code generated by the compiler can fetch the same data multiple times without you being aware of it.
If that data happens to change, your program may become using two or more different versions of the data and get into an inconsistent state. This can lead not only to logically incorrect behavior of the program but also to exploitable security holes in it if it processes untrusted files or files from untrusted locations.
If you care about security, and you should, this is an important scenario to consider.
有两种用途。 这些特别是在嵌入式开发中用得比较多。
编译器不会优化使用 volatile 关键字定义的变量的函数
Volatile 用于访问 RAM、ROM 等中的精确内存位置...这更常用于控制内存映射设备、访问 CPU 寄存器并定位特定内存位置。
请参阅带有程序集列表的示例。
回复:嵌入式开发中 C“易失性”关键字的使用
There are two uses. These are specially used more often in embedded development.
The compiler will not optimise the functions that use variables that are defined with the volatile keyword
Volatile is used to access exact memory locations in RAM, ROM, etc... This is used more often to control memory-mapped devices, access CPU registers and locate specific memory locations.
See examples with assembly listing.
Re: Usage of C "volatile" Keyword in Embedded Development
当您想要强制编译器不优化特定代码序列(例如,编写微基准)时,易失性也很有用。
Volatile is also useful, when you want to force the compiler not to optimize a specific code sequence (e.g. for writing a micro-benchmark).
在我看来,您不应该对
volatile
抱有太多期望。 为了进行说明,请查看Nils Pipenbrinck 的高票答案中的示例。我想说,他的例子不适合
易失性
。易失性
仅用于:阻止通常有用且理想的编译器优化。 它与线程安全、原子访问甚至内存顺序无关。
在该示例中:
gadget->command = command
之前的gadget->data = data
仅在编译器编译的代码中得到保证。在运行时,处理器仍然可以重新排序数据和命令分配,具体取决于处理器架构。 硬件可能会获取错误的数据(假设小工具映射到硬件 I/O)。 数据和命令分配之间需要内存屏障。
In my opinion, you should not expect too much from
volatile
. To illustrate, look at the example in Nils Pipenbrinck's highly-voted answer.I would say, his example is not suitable for
volatile
.volatile
is only used to:prevent compiler optimizations that would normally be useful and desirable. It is nothing about thread safety, atomic access or even memory order.
In that example:
The
gadget->data = data
beforegadget->command = command
only is only guaranteed in the compiled code by the compiler.At run time, the processor may still reorder the data and command assignment, depending on the processor architecture. The hardware could get the wrong data (suppose a gadget is mapped to hardware I/O). A memory barrier is needed between data and command assignment.
在 Dennis Ritchie 设计的语言中,对任何对象(地址尚未获取的自动对象除外)的每次访问都会表现为计算对象的地址,然后在该地址读取或写入存储。 这使得该语言非常强大,但优化机会受到严重限制。
虽然可以禁止编译器假设可寻址对象永远不会以奇怪的方式更改,但这种假设对于 C 程序中的绝大多数对象来说是适当且有用的,并且添加一个适合这种假设的所有对象的限定符。 另一方面,某些程序需要使用某些对象,但这种假设不成立。 为了解决这个问题,该标准规定编译器可能会假设未声明为 volatile 的对象的值不会以超出编译器控制的方式被观察或更改,或者超出合理的编译器的控制范围。理解。
由于不同的平台可能有不同的方式来在编译器的控制之外观察或修改对象,因此这些平台的高质量编译器在对易失性语义的精确处理上应该有所不同,这是适当的。 不幸的是,因为该标准未能建议用于任何特定平台上低级编程的高质量编译器应该以一种能够识别特定读/写操作的任何和所有相关影响的方式处理易失性。在该平台上,许多编译器都未能做到这一点,这使得以高效但不能被编译器“优化”破坏的方式处理后台 I/O 之类的事情变得更加困难。
In the language designed by Dennis Ritchie, every access to any object, other than automatic objects whose address had not been taken, would behave as though it computed the address of the object and then read or wrote the storage at that address. This made the language very powerful, but severely limited optimization opportunities.
While it might have been possible to forbid compilers from assuming that addressable objects would never be changed in weird ways, such an assumption would have been appropriate and useful for the vast majority of objects in C programs, and it would have been impractical to add a qualifier to all the objects for which such assumption would be appropriate. On the other hand, some programs need to use some objects for which such an assumption would not hold. To resolve this issue, the Standard says that compilers may assume that objects which are not declared
volatile
will not have their value observed or changed in ways that are outside the compiler's control, or would be outside a reasonable compiler's understanding.Because various platforms may have different ways in which objects could be observed or modified outside a compiler's control, it is appropriate that quality compilers for those platforms should differ in their exact handling of
volatile
semantics. Unfortunately, because the Standard failed to suggest that quality compilers intended for low-level programming on any particular platform should handlevolatile
in a way that will recognize any and all relevant effects of a particular read/write operation on that platform, many compilers fall short of doing so in ways that make it harder to process things like background I/O in a way which is efficient but can't be broken by compiler "optimizations".易失性表示存储空间可能随时发生变化,并且被用户程序控制之外的事物所改变。
这意味着,如果您引用该变量,程序应始终检查物理地址(即映射的输入FIFO),并且不以缓存方式使用它。
volatile means the storage is likely to change at any time and be changed by something outside the control of the user program.
This means that if you reference the variable, the program should always check the physical address (i.e., a mapped input FIFO), and not use it in a cached way.
简单来说,它告诉编译器不要对特定变量进行任何优化。 映射到设备寄存器的变量由设备间接修改。 在这种情况下,必须使用 volatile。
In simple terms, it tells the compiler not to do any optimisation on a particular variable. Variables which are mapped to device register are modified indirectly by the device. In this case, volatile must be used.
所有内容:
维基百科描述了有关
易失性
的 Linux 内核的文档还对易失性
做了很好的说明:Wikipedia says everything about
volatile
:And the Linux kernel's documentation also makes a excellent note about
volatile
:可以从编译代码外部更改易失性变量(例如,程序可以将易失性变量映射到内存映射寄存器)。
编译器不会对处理易失性变量的代码应用某些优化。 例如,它不会将其加载到寄存器而不将其写入内存。 这在处理硬件寄存器时很重要。
A volatile can be changed from outside the compiled code (for example, a program may map a volatile variable to a memory mapped register).
The compiler won't apply certain optimizations to code that handles a volatile variable. For example, it won't load it into a register without writing it to memory. This is important when dealing with hardware registers.
易失性经常被误解为“禁用优化”、同步对变量的访问或生成内存栅栏。 没有一个是完全如此的。 它所做的只是禁用任何假设变量未从外部上下文更改的编译器优化。
根据 GCC
消除优化效果,
Volatile 做了什么要理解 易失性,我们可以简单地看一下 GCC 生成的代码。 在第一个示例中,我们将 volatile 变量与 argc 相乘并存储在
i
中。请参阅此处使用 O2 的组装:
https://godbolt.org/z/cf4KGjxvP
Volatile 告诉编译器该变量不能被期望在编译时间在访问之间保持相同。 它可能在执行过程中随时发生变化。 编译器被告知禁用死存储消除,因为这样做可能会导致意外行为。
引用该变量而不对其执行任何操作也会生成负载。
https://godbolt.org/z/z9cffTsqb
基本上,编译器会禁用任何消除负载或商店。
这是栅栏吗? 关于海湾合作委员会,是的。
在 GCC 上,易失性似乎充当编译器栅栏。
这段代码:
当添加的顺序改变时,对应的程序集有不同的加载顺序。
参见: https://godbolt.org/z/z9KG3PnbE
所以在GCC上, volatile是编译器内存栅栏,但不会生成栅栏指令来序列化访问。
在 MSVC 上,使用相反的加载生成相同的样本,但更改变量顺序也会影响加载顺序。 MSVC 也将其视为编译器栅栏。
https://godbolt.org/z/snWz6fYa6
Volatile 不做什么
Volatile 不是内存栅栏。 编译器可以将其视为一个,但不能保证。 只要不消除内存访问,就可以自由地重新排序。 这意味着如果您确实需要在两个函数调用之间进行内存访问,则可能不会以这种方式生成。
相反,必须显式使用编译器或实内存栅栏。 除非用锁或栅栏进行控制,否则多个处理器之间的并发访问可能无法工作。
易失性不能保证原子性。 加载和存储将通过单个指令进行,但可能不使用原子操作。 在第一个示例中,通过单个操作增加数字。
结论
Volatile 对变量的行为提供的保证很少。 对于其非常狭窄的范围之外的任何情况,都应将其视为不可移植的。 易失性变量访问应封装在辅助函数内,并且应避免易失性函数参数或返回。
应使用 volatile 的情况包括:
多线程编程绝不应包括易失性,因为获取变量的关键部分可确保在其中允许所有优化,因为没有其他东西可以访问该变量。
Volatile is often misunderstood as "disabling optimizations," synchronizing access to a variable, or generating memory fences. None are entirely the case. All it does is disable any compiler optimizations that assume the variable has not changed from an outside context.
What Volatile Does According to GCC
Eliminating Opimizations Effect
To understand
volatile
, we can simply look at the code generated by GCC. In the first example we will multiply the volatile variable with argc and store ini
.See assembly here with O2:
https://godbolt.org/z/cf4KGjxvP
Volatile tells the compiler that the variable cannot be expected at compile time to have remained the same between accesses. It could change at any point during execution. The compiler is told to disable dead store elimination since doing so could lead to unexpected behavior.
Referencing the variable without doing anything to it also generates a load.
https://godbolt.org/z/z9cffTsqb
Basically, the compiler disables any optimization that eliminates loads or stores.
Is It A Fence? On GCC, Yes.
On GCC, volatile appears to act as a compiler fence.
This code:
When the order of the addition is changed, the corresponding assembly has a different load order.
See: https://godbolt.org/z/z9KG3PnbE
So on GCC, volatile is a compiler memory fence, but no fence instructions are generated to serialize access.
On MSVC, the same sample was generated with the loads in reverse, but changing the variable order did also affect the load order. MSVC also regards it as a compiler fence.
https://godbolt.org/z/snWz6fYa6
What Volatile Does Not Do
Volatile is not a memory fence. The compiler could treat it as one, but there is no guarantee. It is free to reorder memory accesses as long as it does not eliminate them. That means if you really need a memory access to happen between two function calls, it may not be generated that way.
Instead, a compiler or real memory fence must be explicitly used. Concurrent access between multiple processors may not work unless controlled with a lock or fence.
Volatile does not ensure atomicity. The loads and stores will happen with single instructions, but atomic operations may not be used. In the first example, the number was incremented with a single operation.
Conclusion
Volatile provides very few guarantees about the behavior of the variable. It should be considered non-portable for any situation outside its very narrow scope. Volatile variable access should be encapsulated inside helper functions and volatile function parameters or returns should be avoided.
The situations where volatile should be used are:
Multithreaded programming should never include volatile since acquiring a critical section for a variable ensures that within it all optimizations are allowed because nothing else can access the variable.
正如这里许多人正确建议的那样,易失性关键字的流行用途是跳过易失性变量的优化。
在阅读了有关 volatile 的内容后,我想到的、值得一提的最佳优点是——防止在出现
longjmp
时回滚变量。 非本地跳转。这是什么意思?
它只是意味着在执行堆栈展开后将保留最后一个值,以返回到某个先前的堆栈帧; 通常是在某些错误情况下。
由于它超出了这个问题的范围,因此我不会在这里详细介绍 setjmp/longjmp ,但它值得一读; 以及如何使用波动性特征来保留最后的值。
As rightly suggested by many here, the volatile keyword's popular use is to skip the optimisation of the volatile variable.
The best advantage that comes to mind, and worth mentioning after reading about volatile is -- to prevent rolling back of the variable in case of a
longjmp
. A non-local jump.What does this mean?
It simply means that the last value will be retained after you do stack unwinding, to return to some previous stack frame; typically in case of some erroneous scenario.
Since it'd be out of scope of this question, I am not going into details of
setjmp/longjmp
here, but it's worth reading about it; and how the volatility feature can be used to retain the last value.它不允许编译器自动更改变量的值。 易失性变量供动态使用。
It does not allow the compiler to automatically change values of variables. A volatile variable is for dynamic use.