C语言中的volatile
由编译器处理,他告知编译器
/* vol_var.c,
* gcc -O3 -S vol_var.c */
volatile int flag = 0; |
|
void _co_fn1(void) | _co_fn1:
{ | .L3:
while (!flag) | movl flag(%rip), %eax
; | testl %eax, %eax
} | je .L3
| rep ret
|
void _co_fn2(void) | _co_fn2:
{ | movl $1, flag(%rip)
flag = 1; | ret
} |
从C语句对应的汇编语句可以看出,每次访问由voltaile
修饰的 flag 变量时,都会在内存/一致性缓存中发生。
以下是不用volatile
修饰 flag 变量时的版本对比。
/* nvol_var.c,
* gcc -O3 -S nvol_var.c */
int flag = 0; | _co_fn1:
| movl flag(%rip), %eax
void _co_fn1(void) | testl %eax, %eax
{ | jne .L1
while (!flag) | .L3:
; | jmp .L3
} | .L1:
| rep ret
void _co_fn2(void) |
{ | _co_fn2:
flag = 1; | movl $1, flag(%rip)
} | ret
经过编译器优化后,只会从内存中读取一次 flag。如此,就算 _co_fn2 被并发/并行/跳转执行置 flag为1后,_co_fn1 也不会终止。
/* vol_st.c,
gcc -O3 -S vol_st.c */
int chan1; |
volatile int chan2; |
|
void _co_fn1(void) | _fn1:
{ | movl $17, chan2(%rip)
| movl $34, chan2(%rip)
chan2 = 0x11; | ret
chan2 = 0x22; |
} | _fn2:
| movl $34, chan1(%rip)
void _co_fn2(void) | ret
{ |
chan1 = 0x11; | _fn3:
chan1 = 0x22; |
} | cmpl $17, chan1(%rip)
| je L6
void _co_fn3(void) | rep ret
{ |
if (chan1 == 0x11) | .L6:
chan2 = 0x33; | movl $17, chan2(%rip)
} | ret
处于O3层优化的编译器认为 _co_fn2 中的"chan1 = 0x11"需被优化掉——随即会被 0x22 赋值覆盖。但这样会导致并发/并行/跳转的 _co_fn3 中的if语句永远也得不到执行。而经过volatile
修饰的 chan2 没有受到优化的任何影响。
编译器会在一定程度上打乱互不相干语句的顺序,以最大化CPU流水线。若需避免这种优化,需要用volatile
来修饰语句中的变量或语句(如内联汇编)。
/* vol_storder.c,
gcc -O3 -S vol_storder.c */
volatile int a[5000]; |
volatile int flag = 0; |
|
void *_fn1(void) | _fn1:
{ | movl $0, a(%rip)
a[0] = 0; | movl $1, a+4(%rip)
a[1] = 1; | movl $2, a+8(%rip)
a[2] = 2; | movl $1, flag(%rip)
// something done | movl $3, a+12(%rip)
flag = 1; | ret
|
a[3] = 3; | _fn2:
} | movl flag(%rip), %eax
| testl %eax, %eax
void _fn2(void) | je .L2
{ | movl $10, a+36(%rip)
if (flag) { | .L2:
// base something done | rep ret
a[9] = 10; |
} |
} |
可见到,经volatile
修饰变量形成语句的顺序不会被编译器打乱。
接下来再看看去除volatile
的结果。
int a[5000]; |
int flag = 0; |
|
void *_co_fn1(void) | _co_fn1:
{ | movdqa .LC0(%rip), %xmm0
a[0] = 0; | movl $1, flag(%rip)
a[1] = 1; | movdqa %xmm0, a(%rip)
a[2] = 2; | ret
// something done |
flag = 1; | _co_fn2:
| movl flag(%rip), %eax
a[3] = 3; | testl %eax, %eax
} | je .L2
| movl $10, a+36(%rip)
void _co_fn2(void) | .L2:
{ | rep ret
if (flag) { | .LC0:
// base something done | .long 0
a[9] = 10; | .long 1
} | .long 2
} | .long 3
处于O3优化级别的编译器为了让 a[0] ~ a[3] 能局部在cache中并最大化CPU流水线而将 a[3] 与 a[0] ~ a[02] 放在一堆赋值,从而导致a[0] ~ a[2]的赋值在 flag 之后,所以在并发/并行/中断的 _co_fn2 中检测到 flag 为真时,a[0] ~ a[2] 不一定被赋了值。
由于volatile
会在一定程度上(寄存器作缓存、最大化流水线)阻碍编译器对源程序的优化,所以我们最好只在需要避免编译器对源程序作相应优化操作时才用volatile
。