最近发现身边很多同事对volatile理解有些偏差,我也本着求证的态度做了一番调查写下此文。
一些文章都会下这样定义volatile:“volatile修饰的变量会取消内存优化,使cpu每次都从内存中读写变量。”这样的说法大体上是成立的,但是严格的说volatile并不能越过cache机制直接访问DDR,原因是cache机制是对用户透明的,即使在kernel中要想操作cache也需要通过专用的api才能做到,具体api这里不做过多讨论。那么volatile到底做了什么呢?说明这个事情需要用到汇编。
我们先看一个非常简单的c代码,代码里分别读取了volatile指针和普通指针的值输出,我们看看编译器是怎么处理volatile修饰的。
int main(int argc, char const *argv[])
{
int *buf1;
volatile int *buf2;
int i;
buf2 = buf1 = (int*)malloc(4);
for(i=0; i<10; ++i) {
printf("0x%x 0x%x\n", *buf1, *buf2);
}
return 0;
}
使用O3优化编译,然后反汇编出指令。
arm-linux-gnueabi-gcc test.c -o test -O3
arm-linux-gnueabi-objdump -D test | less
得到汇编指令如下:
00010340 <main>:
10340: e92d41f0 push {r4, r5, r6, r7, r8, lr}
10344: e3a00004 mov r0, #4
10348: ebfffff0 bl 10310 <malloc@plt>
1034c: e3a0400a mov r4, #10
10350: e1a05000 mov r5, r0
//读取buf1的值到r7
10354: e5907000 ldr r7, [r0]
//读取printf的字符串到r6
10358: e59f601c ldr r6, [pc, #28] ; 1037c <main+0x3c>
//for循环开始 {
//读取buf2到r2
1035c: e5952000 ldr r2, [r5]
//buf1传参给printf
10360: e1a01007 mov r1, r7
//字符串传参给printf
10364: e1a00006 mov r0, r6
//调用printf
10368: ebffffe5 bl 10304 <printf@plt>
1036c: e2544001 subs r4, r4, #1
10370: 1afffff9 bne 1035c <main+0x1c>
// } for循环结束
10374: e1a00004 mov r0, r4
10378: e8bd81f0 pop {r4, r5, r6, r7, r8, pc}
1037c: 00010518 andeq r0, r1, r8, lsl r5
从这个例子我们看到编译器在处理volatile修饰的buf2时每次的都会用ldr读取指针,而普通指针buf1只会在进入循环前读取一次,volatile修饰的变量可以避免程序在运行过程中指针内容被修改的问题,但是ldr指令不可能越过cache机制所以严格的说并不是直接访问内存。