C volatile 使用基础理解

C语言中的volatile由编译器处理,他告知编译器

1 每次访问由 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 也不会终止。

2 不要优化与 volatile 修饰/相关的语句

/* 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 没有受到优化的任何影响。

3 不要乱序执行由 volatile 修饰的语句

编译器会在一定程度上打乱互不相干语句的顺序,以最大化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

你可能感兴趣的:(都市)