volatile关键字

今天快下班前听到他们正在讨论说某个bug可能是编译器优化导至的bug, 我当时正在忙其他事也就没有参与讨论,下班回来的路上想到了编译器优自然而然就想到了volatile关键字。

随手上百度搜了一下,得到的结论令我大吃一惊,说什么的都有,有说阻止编译器优化,还有说用于多线程并发,还有说影响cpu cache,总之说什么的都有。其他的我不敢说,但是至少编译器级别的优化还达不到cpu cache的层次,毕竟我搞过一段时间的无锁算法的,对cpu的cache原理有一定的了解。

至于真相到底是什么,也只能自力更生,验证一下了,虽然我平时尽量不用代码或尽量使用伪码说明问题,奈何这是一篇验证性的文章,相关的C代码和汇编肯定是少不了的。

我写了段代码分别测试了一下局部变量,局部指针变量,全局变量,静态全局变量分别是volatile和非volatile的情况,使用gcc -O2 -S a.c来编译以下C代码:

#include "stdlib.h"
#include "stdio.h"

int gg = 1;
static int xx = 1;

int test()
{
        gg = 0;
        xx = 0;
        return 0;
}

int main()
{
        //test the local variable
        int a = 0;
        a += 3;
        a += 4;
        printf("%d\n", a);

        //test the volatile local variable
        volatile int b = 0;
        b += 3;
        b += 4;
        printf("%d\n", b);

        //test the local memory pointer
        int *c = (int *)malloc(4);
        *c = 0;
        *c += 3;
        *c += 5;
        printf("%p, %d\n", c, *c);
        //test the variable local memory pointer
        volatile int *d = (int *)malloc(4);
        *d = 0;
        *d += 3;
        *d += 5;
        

        //test the global variable
        while (gg) {
                printf("helloworld\n");
        }

        //test the static global variable
        while (xx) {
                printf("helloworld\n");
        }

        return a;
}

产生的汇编代码如下:

.section __TEXT,__text,regular,pure_instructions
.globl _test
.align 4, 0x90
_test: ## @test
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp2:
.cfi_def_cfa_offset 16
Ltmp3:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp4:
.cfi_def_cfa_register %rbp
movl $0, _gg(%rip)
movb $1, _xx(%rip)
xorl %eax, %eax
popq %rbp
retq
.cfi_endproc

.globl _main
.align 4, 0x90
_main: ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp8:
.cfi_def_cfa_offset 16
Ltmp9:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp10:
.cfi_def_cfa_register %rbp
pushq %rbx
pushq %rax
Ltmp11:
.cfi_offset %rbx, -24
leaq L_.str(%rip), %rbx
movl $7, %esi
xorl %eax, %eax
movq %rbx, %rdi
callq _printf
movl $0, -12(%rbp)
addl $3, -12(%rbp)
addl $4, -12(%rbp)
movl -12(%rbp), %esi
xorl %eax, %eax
movq %rbx, %rdi
callq _printf
movl $4, %edi
callq _malloc
movq %rax, %rcx
movl $8, (%rcx)
leaq L_.str1(%rip), %rdi
movl $8, %edx
xorl %eax, %eax
movq %rcx, %rsi
callq _printf
movl $4, %edi
callq _malloc
movl $0, (%rax)
addl $3, (%rax)
addl $5, (%rax)
cmpl $0, _gg(%rip)
je LBB1_3
## BB#1:
leaq L_str3(%rip), %rbx
.align 4, 0x90
LBB1_2: ## %.lr.ph4
## =>This Inner Loop Header: Depth=1
movq %rbx, %rdi
callq _puts
cmpl $0, _gg(%rip)
jne LBB1_2
LBB1_3: ## %.preheader
movb _xx(%rip), %al
testb %al, %al
jne LBB1_6
## BB#4:
leaq L_str3(%rip), %rbx
.align 4, 0x90
LBB1_5: ## %.lr.ph
## =>This Inner Loop Header: Depth=1
movq %rbx, %rdi
callq _puts
movzbl _xx(%rip), %eax
cmpl $1, %eax
jne LBB1_5
LBB1_6: ## %._crit_edge
movl $7, %eax
addq $8, %rsp
popq %rbx
popq %rbp
retq
.cfi_endproc

.section __DATA,__data
.globl _gg ## @gg
.align 2
_gg:
.long 1 ## 0x1

.zerofill __DATA,__bss,_xx,1,0 ## @xx
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "%d\n"

L_.str1: ## @.str1
.asciz "%p, %d\n"

L_str3: ## @str3
.asciz "helloworld"

.subsections_via_symbols

对比局部变量a和volatile局部变量b可以看出:
a被优化成了一个寄存器%esi, 而关于a的操作被优化成一个常量赋值。
而b的所有操作均没有被优化,b的每次存取操作均是直接操作栈上内存。

对比局部指针变量c和volatile局部指针变量d:
向*c和*d的赋值的最后一步均为直接写入内存,但是对于c指针所指向内存的加法则被优化成了常量赋值,而对于d指针所指向内存的所有操作均没有被优化。

再看对于全局变量和静态全局变量的访问和修改,均是直接读写或修改内存,并不使用cpu的寄存器做优化。

那么使可以得出以下结论:
volatile关键字使得所有操作此变量的操作均不会被优化(如优化为寄存器或操作合并)
即使对于对内存和全局变量进行一些优化操作(如将连续的加合并为一个常量赋值),在最后一个操作时一定会将所得的值存入内存(即其效果一定看起来等效于原来的语句)。
也就是说除了不想编译器合并对某个变量的操作和不想编译器将某一局部变量优化成寄存器,我们几乎没有使用volatile的地方(这一准则同样适用于嵌入式领域)

现在开始反驳一些结论:
1. 多线程通过变量交互,需要使用volatile变量。
如果使用变量进行多线程通信, 那么这个变量必然是全局的,如上所述全局变量是会即时写入内存的,并不存在所谓的寄存器残留问题, 也就不需要使用volatile.

2. 在中断程序中需要修改变量时,此变量一定是volatile变量(嵌入式系统)
同结论1相似,如果中断程序可以访问此变量,而应用程序也可以访问些变量,则此变量至少一定是静态全局变量,同样不需要volatile。

3. 影响cpu cache。
除了使用cpu自带的无锁汇编指令或lock汇编指令,其他的汇编是不能够影响cpu cache的。关于这个问题是可以参考Intel参考手册的。

ps.虽然我一向不喜欢做大自然的搬运工,但还是做了一次。



发表评论