Volatile关键字

20 May 2014

1 volatile

防止编译器的某些优化措施, 在c/c++中的作用,

  1. 可以访问内存映射设备
  2. setjmp和longjmp使用
  3. 信号处理函数中使用sigatomict变量

需要注意的问题:不是原子操作,不能保证happens-before的关系,后面的Java按照何文所说是可以的。

1.1 volatile in c

wiki中的例子,说明了其一个用法,向编译器声明这个变量是会变化的:即使在目前的代码中可能没有变化,但是会被其他的外部因素转变(可以认为是存在外部影响的副作用,造成该变量的改变)。

后面的汇编代码比较中可以看到,没有volatile关键字的变量,编译器会认为不会存在外部环境的介入影响变量值,故,一旦存在确定的数值,都会直接代替。存在volatile后,由于表明该变量会有外部副作用改变,编译器在任何一个时刻都默认这个值可能会改变,都需要从地址中重新取值。

在c++11标准中,其只用于访问硬件,线程内部的通讯都会使用标准库的 std::atomic<T>。

1.2 针对kernel代码

In other words, they have been known to treat volatile types as a sort of easy atomic variable, which they are not. The use of volatile in kernel code is almost never correct;

在有锁的情况下,不需要在加锁的代码区间中再增加volatile关键字,因为编译器已经知道优化范围,会在这一区间中进行优化,而非完全的去除优化,从而降低代码效率。1 最初volatile的目的是用于处理memory-mapped I/O registers的,但是在kernel中,对寄存器的访问也是需要加锁的,但是I/O memory访问是通过函数(accessor functions)进行的,而非直接通过指针获得内存数据。而函数中已经做了措施防止不必要的优化。2

部分可能出现的情况:

  1. 上面的accessor function内可能会使用。
  2. 内联的汇编代码,改变了内存,但是没有可见的副作用,可能会被gcc优化掉。
  3. jiffies变量,每次都会变化,而且读取也不用加锁。但是其他的类似变量还是最好用锁。
  4. 指针可能会被IO设备改变的情况。

可见使用的绝大部分场景都是和硬件IO交互的时候使用,也即编译器无法控制的因素而且也没有什么固定的协议可以遵循,如果有了固定的协议在编译期就可以判断遵循,其实编译器也可以处理。

1.3 何登成一文

文中的前两个对比说明的就是易变性和不可优化,其实最后还是集中在不可优化这一点上。如果我们将寄存器也看作是内存的缓存,那么编译器进行的寄存器的分配等等都是针对缓存的优化算法,如果将寄存器、L1等等cache都简化掉【而这些其实都是属于优化措施】,那么剩下的很显然就不存在易变性这个问题。

第三个的顺序性问题:

  • 首先涉及的是编译器的指令重排问题,编译后的代码是不能认为就是源代码中的执行顺序。除非可以在这段代码区间内去除代码重排的功能。所以设定所有的变量都是volatile可以解决这个问题,或者关闭优化,其实这符合kernel中的说明,关了优化影响性能,加锁能保证,而且还可以针对性的优化。
  • 其次,硬件上的CPU优化也会乱序执行指令。这样最后也是最弱的前提也消失掉了,完全失去使用的意义。除非不要编译器的优化和硬件优化。

最后还是优化措施的引入造成的数据一致性的问题,解决的方法自然是增加优化的控制范围,减少和控制不想要的优化。后面历史那段貌似就是说本用于与memory-mapped IO设备交互的需要,编译器对硬件级别的副作用难以识别。

1.4 Java

Java中,保证了上面的第三个顺序性问题,所以可以拿来用,应该是1.5版本以后。回头再仔细看看….

综上,Java里可以用,c/c++里要是有用的,可以考虑直接当bug处理,毕竟需要使用的场景概率太小了。

Footnotes:

1

这里不仅仅针对的是kernel,应用层也一样。

2

另有一个busy-waiting的情况,待查。