1.”volatile”的语义
- 表示
flag可能被硬件、中断、多线程或其他外部因素修改
- 编译器不能假设其值在两次访问之间保持不变
- 禁止优化:如删除“看似无用”的读取、合并多次读取、将变量放入寄存器等
2.从汇编角度看
样例代码Code1: (无volatile)
1 2 3 4 5
| int flag = 1; int sum = 0; for (int i = 0; i < 1000; i++) { sum += flag; }
|
可能生成的汇编代码
1
| movl $1000, %eax ; sum = 1000 * 1
|
完全跳过循环和内存访问,因为编译器认为flag不会变。
样例代码Code2: (有volatile)
1 2 3 4 5
| volatile int flag = 1; int sum = 0; for (int i = 0; i < 1000; i++) { sum += flag; }
|
可能生成的汇编代码
1 2 3 4 5 6 7 8 9 10
| movl $0, %ebx ; sum = 0 movl $0, %ecx ; i = 0 .L1: cmpl $1000, %ecx jge .L2 movl flag(%rip), %eax ; ← 每次都从内存读取 flag! addl %eax, %ebx ; sum += flag incl %ecx jmp .L1 .L2:
|
- 每次循环都执行
movl flag(%rip), %eax从内存加载flag
- 即使
flag在循环中未被本程序修改,编译器也不敢假设其值不变
3.样例代码实际输出的汇编码
样例代码CodeA: (无volatile)
1 2 3 4 5 6 7 8
| int main(){ int flag = 1; int sum = 0; for (int i = 0; i < 1000; i++) { sum += flag; } return 0; }
|
汇编代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| .file "codeA.cpp" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $1, -4(%rbp) movl $0, -12(%rbp) movl $0, -8(%rbp) jmp .L2 .L3: movl -4(%rbp), %eax addl %eax, -12(%rbp) addl $1, -8(%rbp) .L2: cmpl $999, -8(%rbp) jle .L3 movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (GNU) 15.2.1 20250813" .section .note.GNU-stack,"",@progbits
|
样例代码CodeB: (有volatile)
1 2 3 4 5 6 7 8
| int main(){ volatile int flag = 1; int sum = 0; for (int i = 0; i < 1000; i++) { sum += flag; } return 0; }
|
汇编代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| .file "codeB.cpp" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $1, -12(%rbp) movl $0, -8(%rbp) movl $0, -4(%rbp) jmp .L2 .L3: movl -12(%rbp), %eax addl %eax, -8(%rbp) addl $1, -4(%rbp) .L2: cmpl $999, -4(%rbp) jle .L3 movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (GNU) 15.2.1 20250813" .section .note.GNU-stack,"",@progbits
|
使用O2后
CodeA:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| .file "codeA.cpp" .text .section .text.startup,"ax",@progbits .p2align 4 .globl main .type main, @function main: .LFB0: .cfi_startproc xorl %eax, %eax ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (GNU) 15.2.1 20250813" .section .note.GNU-stack,"",@progbits
|
CodeB:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| .file "codeB.cpp" .text .section .text.startup,"ax",@progbits .p2align 4 .globl main .type main, @function main: .LFB0: .cfi_startproc movl $1, -4(%rsp) movl $1000, %eax .p2align 4 .p2align 4 .p2align 3 .L2: movl -4(%rsp), %edx movl -4(%rsp), %edx subl $2, %eax jne .L2 xorl %eax, %eax ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (GNU) 15.2.1 20250813" .section .note.GNU-stack,"",@progbits
|
4.从汇编看
从开启O2的汇编里看,CodeA由于没有使用”volatile”关键字,for循环直接被优化了
1 2 3
| main: xorl %eax, %eax ret
|
为什么?
flag是编译期常量(值为 1)
sum未被使用(无return sum或输出)
- 编译器判定循环无副作用 -> Dead Code Elimination
而CodeB中
1 2 3 4 5 6 7 8 9 10
| main: movl $1, -4(%rsp) # flag = 1 存入栈 movl $1000, %eax # 计数器初始化 .L2: movl -4(%rsp), %edx # <- 强制从内存读取 flag movl -4(%rsp), %edx # <- 再次读取(可能因对齐或语义保留) subl $2, %eax jne .L2 xorl %eax, %eax ret
|
- 循环被保留,尽管
flag值不变
- 每次循环都执行
movl -4(%rsp), %edx
- 即使
%edx未被使用,编译器也不敢删除
- 因为
volatile表示“每次访问都有潜在副作用”
- 循环步长为 2(
subl $2, %eax)可能是编译器尝试合并两次加法(sum += flag + flag),但因”volatile”仍需两次独立读取
5.写在最后
- “volatile”不是运行时特性,而是对编译器优化器的约束。
在-O0(无优化)下,普通变量与”volatile”行为相似(都访问内存),差异不明显
- 在
-O2等优化级别下,”volatile” 强制保留所有内存访问,确保程序行为符合预期