C++中的"volatile"关键字

1.”volatile”的语义

1
volatile int flag = 0;
  • 表示flag可能被硬件、中断、多线程或其他外部因素修改
  • 编译器不能假设其值在两次访问之间保持不变
  • 禁止优化:如删除“看似无用”的读取、合并多次读取、将变量放入寄存器等

2.从汇编角度看

样例代码Code1: (无volatile)
1
2
3
4
5
int flag = 1;
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += flag; // 编译器可能优化为 sum += 1
}

可能生成的汇编代码

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; // 必须每次从内存读取 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.样例代码实际输出的汇编码

Compile on Archlinux WSL

样例代码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

为什么没有区别?

因为没有使用编译器优化,O0模式

使用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” 强制保留所有内存访问,确保程序行为符合预期