目录

1.   volatile 本身的含义

2.   volatile 在计算机领域中的含义

3.   volatile 在C和C++ 中的含义

4.   volatile 与多线程

5.   C语言中内存映射I/O的示例

6.   C中编译对比


1.   volatile 本身的含义

         从字典词义上来说,volatile 词义为“易变的,易失的。”

2.   volatile 在计算机领域中的含义

         在计算机编程中,如果一个变量的值能够被当前执行线程以外的实体异步读取或修改,则称该变量为“易变”变量(volatile variable)。易变变量的值可能会因多种原因而发生自发变化,例如:与其他线程共享数据;与异步信号处理程序共享数据;或者通过内存映射 I/O(Memory-Mapped I/O)访问硬件设备(即通过读写内存来接收和发送来自外设的消息)。在支持 volatile 关键字的各种编程语言中,对这些应用场景的支持程度各不相同变量的易变性可能会影响函数调用约定,以及变量的存储、访问和缓存方式

3.   volatile 在C和C++ 中的含义

        在 C 和 C++ 中,volatile 是一种类型限定符(类似于 const),并且是类型(例如变量或字段的类型)的一部分。( 注:这一点无疑,两种语言中均有此关键字 。)

    C 和 C++ 中 volatile 关键字的行为,有时会被描述为抑制优化编译器的某些优化操作:(1) 不移除现有的 volatile 读写操作;(2) 不添加新的 volatile 读写操作;(3) 不重排 volatile 读写操作。然而,这种定义仅是为了便于初学者理解而给出的近似说法,在编写实际生产代码时不应依赖这一近似定义

    在 C 语言(以及随后的 C++)中,volatile关键字旨在:

●  允许访问内存映射 I/O 设备(内存变量对应设备寄存器)。

●  允许在执行 longjmp 时保留变量值。

●  允许通过 volatile sig_atomic_t 对象在信号处理程序与程序的其余部分之间共享变量值。

C 和 C++ 标准允许编写可移植代码,利用 volatile 对象在 longjmp 操作期间共享数据,也允许利用 volatile sig_atomic_t 对象在信号处理程序与程序的其余部分之间共享数据。除此之外,在 C 和 C++ 中使用 volatile 关键字均属于不可移植或不正确的做法。特别是,针对内存映射 I/O 设备使用 volatile 关键字编写代码,本质上是不可移植的,且始终需要对特定的目标 C/C++ 实现及平台有深入的了解。

4.   volatile 与多线程

    一个常见的误解是认为 volatile 关键字在 C 和 C++ 的可移植多线程代码中很有用实际上,在任何多线程场景下,C 和 C++ 中的 volatile 关键字从未作为一种有用且可移植的工具发挥过作用。与 Java 和 C# 编程语言不同,C 和 C++ 中针对 volatile 变量的操作并非原子操作也不具备足够的内存排序保证(即内存屏障)。大多数 C 和 C++ 编译器、链接器及运行时环境根本不提供必要的内存排序保证,使得 volatile 关键字无法在任何多线程场景中发挥作用。在 C11 和 C++11 标准出台之前,程序员编写多线程代码时,不得不依赖特定实现或平台(如 POSIX 和 WIN32)所提供的保证。随着现代 C11 和 C++11 标准的引入,程序员现在可以使用诸如 std::atomic<T> 模板等新的可移植构造来编写可移植的多线程代码

5.   C语言中内存映射I/O的示例

在本例中,代码将存储在 foo 中的值设为 0。随后,它开始反复轮询该值,直到其变为 255 (注:在内存映射I/O中,foo的值由其它外部系统写到I/O口,从而映射到变量foo,即其值由外部赋予,而不是程序中改变,比如上位机写入下位机寄存器值)。

static int foo;

void bar(void) {
    foo = 0;

    while (foo != 255) {}
}

优化编译器会发现,没有任何其他代码能够改变存储在 foo 中的值,并据此假定该值将始终保持为 0。因此,编译器会将函数体替换为一个类似如下的无限循环:

void bar_optimized(void) {
    foo = 0;

    while (true) {}
}

若要防止编译器进行此项优化,可以使用 volatile 关键字

static volatile int foo;

void bar(void) {
    foo = 0;

    while (foo != 255) {}
}

volatile 关键字阻止编译器将读取操作移出循环,因此代码能够检测到变量 foo 的预期变化。

(1) 下面,我们在 Windows X64平台下用 VS2026 来编译该程序,看看有什么变化,在VS2026 的配置选项中,我们配置为最大优化,并生成Debug版。

在不加 volatile 关键字的情况下:

static int foo;

void bar(void) {

         foo = 0;

         while (foo != 255) {}

}

调用:

          bar();

我们看看生成的反汇编代码:

          bar();

00007FF6821F6E98  call        bar (07FF6821D37BAh) 

void bar(void) {

00007FF6821F2FB0  nop 

         foo = 0;

         while (foo != 255) {}

00007FF6821F2FB2  jmp         bar+2h (07FF6821F2FB2h) 

从以上代码可以看出,编译器会认为,在本程序中,foo 的值不会改变,甚至都没有对foo初始化生成代码,认为 foo != 255 总是成立,因此都没有去检测 foo 的值是否有变化,直接生成一个死循环语句

在加 volatile 关键字的情况下:

          bar();

00007FF6F6267474  mov         dword ptr [foo (07FF6F64A2A38h)],0 

00007FF6F626747E  mov         eax,dword ptr [foo (07FF6F64A2A38h)] 

00007FF6F6267484  cmp         eax,0FFh 

00007FF6F6267489  jne         wmain+0Eh (07FF6F626747Eh)

汇编代码说明:

00007FF6F6267474  mov         dword ptr [foo (07FF6F64A2A38h)],0 

这一句是对变量 foo的初始化,可以看出并没有把它优化掉。

00007FF6F626747E  mov         eax,dword ptr [foo (07FF6F64A2A38h)] 

这一句将 foo 的值移入寄存大 eax 。

00007FF6F6267484  cmp         eax,0FFh 

将eax 的值与 0FFh (255) 的值进行比较,从此可以看出,所生成的代码会去检测 foo的值,看其是否发生变化。

00007FF6F6267489  jne         wmain+0Eh (07FF6F626747Eh)

如果值不相等,则继续检测 foo 的值。

从以上可以看出,编译器不会对涉及变量 foo 的操作做本质上的优化。

(2)  现在看看在 CentOS 8.1(Linux) 中使用 gcc编译的情况。

没有加 volatile 的情况下,生成的汇编文件。

static int foo;

void bar(void) {

         foo = 0;

         while (foo != 255) {}

}

我们将文件命名为 example.c

gcc -S -O3 -masm=intel example.c -o example.s

生成的汇编文件为:

       .file   "example.c"

       .intel_syntax noprefix

       .text

       .p2align 4,,15

       .globl  bar

       .type   bar, @function

bar:

.LFB22:

       .cfi_startproc

       .p2align 4,,10

       .p2align 3

.L2:

       jmp     .L2

       .cfi_endproc

.LFE22:

       .size   bar, .-bar

       .ident  "GCC: (GNU) 8.3.1 20190507 (Red Hat 8.3.1-4)"

       .section        .note.GNU-stack,"",@progbits

.L2:

jmp    .L2

这句可以看出,直接被优化掉了,没有判断 foo 的值。

volatile 的情况下,生成的汇编文件。

       .file   "example.c"

       .intel_syntax noprefix

       .text

       .p2align 4,,15

       .globl  bar

       .type   bar, @function

bar:

.LFB22:

       .cfi_startproc

       mov     DWORD PTR foo[rip], 0

       .p2align 4,,10

       .p2align 3

.L2:

       mov     eax, DWORD PTR foo[rip]

       cmp     eax, 255

       jne     .L2

       ret

       .cfi_endproc

.LFE22:

       .size   bar, .-bar

       .local  foo

       .comm   foo,4,4

       .ident  "GCC: (GNU) 8.3.1 20190507 (Red Hat 8.3.1-4)"

       .section        .note.GNU-stack,"",@progbits

注意生成的汇编代码:

       mov     eax, DWORD PTR foo[rip]

       cmp     eax, 255

       jne     .L2

以上汇编代码可以看出,编译器没有对 foo 进行优化,每一次执行都判断了 foo的值。

6.   C中编译对比

    以下 C 语言程序及对应汇编代码片段展示了 volatile 关键字如何影响编译器的输出结果。

    观察汇编代码可以明显发现,涉及 volatile 对象的代码更为冗长,这是为了满足 volatile 对象的特性要求。volatile 关键字会阻止编译器对相关代码进行优化,从而确保对 volatile 变量的每一次赋值和读取操作都对应一次实际的内存访问(注:这种表述很流行,但这种说法是不正确的,因为volatile阻止优化,因此变量一定会占用内存栈,而不是优化为某个立即数,因此对变量的赋值和访问就老初按语句来,没有被优化当然是访问内存)。若不使用 volatile 关键字,编译器便会认为无需在每次使用变量时都从内存重新读取,因为此时通常不存在其他线程或进程向该变量所在的内存位置进行写入操作的情况( 注:这种表述同样很流行,但此说不正确的,只需牢记volatile修饰的变量必占用内存栈,任何对其操作都不会优化即可,不宜过度解读,以免引起混淆)。

(1) 在 Windows X64 平台。

不使用volatile关键字:

void FunNoVolatile()
{
	// 永远不会在栈上创建这些变量
	int a = 10;
	int b = 100;
	int c = 0;
	int d = 0;

	/*
		`printf` 将使用参数 `"%d"` 和 `110`(编译器会计算 `a+b` 的和)进行调用,因此不会产生运行时执行加法运算的开销。
	*/
	printf("\n no valiatile a + b = %d \n", a + b);

	/*
	这段代码会因优化而被移除,但在调用 `printf` 时,仍能观察到 `c` 和 `d` 变为 100 产生的影响。
	*/
	a = b;
	c = b;
	d = b;

	/*
	编译器将生成调用 printf 的代码,传入的参数为 "%d" 和 200。
	*/
	printf("\n no volatile: c + d = %d \n", c + d);
}

对应汇编代码:

void FunNoVolatile()

{

00007FF70F4A81E0  sub         rsp,28h 

          // 永远不会在栈上创建这些变量

          int a = 10;

          int b = 100;

          int c = 0;

          int d = 0;

          /*

                    `printf` 将使用参数 `"%d"` 和 `110`(编译器会计算 `a+b` 的和)进行调用,因此不会产生运行时执行加法运算的开销。

          */

          printf("\n no valiatile a + b = %d \n", a + b);

00007FF70F4A81E4  mov         edx,6Eh 

00007FF70F4A81E9  lea         rcx,[string "\n no valiatile a + b = %d \n" (07FF70F69F7A0h)] 

00007FF70F4A81F0  call        printf (07FF70F492950h) 

          /*

          这段代码会因优化而被移除,但在调用 `printf` 时,仍能观察到 `c` 和 `d` 变为 100 产生的影响。

          */

          a = b;

          c = b;

          d = b;

          /*

          编译器将生成调用 printf 的代码,传入的参数为 "%d" 和 200。

          */

          printf("\n no volatile: c + d = %d \n", c + d);

00007FF70F4A81F5  mov         edx,0C8h 

00007FF70F4A81FA  lea         rcx,[string "\n no volatile: c + d = %d \n" (07FF70F69F860h)] 

}

00007FF70F4A8201  add         rsp,28h 

00007FF70F4A8205  jmp         printf (07FF70F492950h) 

说明:

从以上汇编代码可以看出,4个变量并没有占用内存栈。

00007FF70F4A81E4  mov         edx,6Eh 

6Eh = 110

编译器将 a + b = 110 直接优化成 110,然后作为立即数生成一条语句,即将 110 移入寄存器 edx ,作为参数传递给调用函数 printf 。

同理,下面的那句 c + d = C8 作为立即数生成一个语句:

00007FF70F4A81F5  mov         edx,0C8h 

使用 volatile 关键字:

void FunWithVolatile()
{
	// 这些变量会使用栈存储
	volatile int a = 10;
	volatile int b = 100;
	volatile int c = 0;
	volatile int d = 0;

	printf("volatile: \n a + b = %d \n", a + b);

	a = b;
	c = b;
	d = b;

	printf("volatile: \n c+d = %d \n", c + d);
}

生成的汇编代码:

void FunWithVolatile()

{

00007FF71CEA8234  sub         rsp,28h 

          // 这些变量会使用栈存储

          volatile int a = 10;

00007FF71CEA8238  mov         dword ptr [rsp+38h],0Ah 

          volatile int b = 100;

          volatile int c = 0;

          volatile int d = 0;

          printf("volatile: \n a + b = %d \n", a + b);

00007FF71CEA8240  lea         rcx,[string "volatile: \n a + b = %d \n" (07FF71D09F768h)] 

00007FF71CEA8247  mov         dword ptr [rsp+30h],64h 

00007FF71CEA824F  mov         dword ptr [rsp+48h],0 

00007FF71CEA8257  mov         dword ptr [rsp+40h],0 

00007FF71CEA825F  mov         edx,dword ptr [b] 

00007FF71CEA8263  mov         eax,dword ptr [a] 

00007FF71CEA8267  add         edx,eax 

00007FF71CEA8269  call        printf (07FF71CE92950h) 

          a = b;

00007FF71CEA826E  mov         eax,dword ptr [b] 

          c = b;

          d = b;

          printf("volatile: \n c+d = %d \n", c + d);

00007FF71CEA8272  lea         rcx,[string "volatile: \n c+d = %d \n" (07FF71D09F788h)] 

00007FF71CEA8279  mov         dword ptr [a],eax 

00007FF71CEA827D  mov         eax,dword ptr [b] 

00007FF71CEA8281  mov         dword ptr [c],eax 

00007FF71CEA8285  mov         eax,dword ptr [b] 

00007FF71CEA8289  mov         dword ptr [d],eax 

00007FF71CEA828D  mov         edx,dword ptr [d] 

00007FF71CEA8291  mov         eax,dword ptr [c] 

00007FF71CEA8295  add         edx,eax 

}

00007FF71CEA8297  add         rsp,28h 

          c = b;

          d = b;

          printf("volatile: \n c+d = %d \n", c + d);

00007FF71CEA829B  jmp         printf (07FF71CE92950h) 

说明:从以上可以看出,4 个变量都占用了内存栈,而且每一次计算都老老实实从内存读取。

(2) 在 Linux X64 平台(CentOS 8.1,gcc)。

不使用volatile关键字:

 .file   "example.c"

 .intel_syntax noprefix

 .text

 .section .rodata.str1.1,"aMS",@progbits,1

.LC0:

 .string "\n no valiatile a + b = %d \n"

.LC1:

 .string "\n no volatile: c + d = %d \n"

 .text

 .p2align 4,,15

 .globl  FunNoVolatile

 .type   FunNoVolatile, @function

FunNoVolatile:

.LFB22:

 .cfi_startproc

 sub     rsp, 8

 .cfi_def_cfa_offset 16

mov    esi, 110

 mov     edi, OFFSET FLAT:.LC0

 xor     eax, eax

 call    printf

mov    esi, 200

 xor     eax, eax

 add     rsp, 8

 .cfi_def_cfa_offset 8

 mov     edi, OFFSET FLAT:.LC1

 jmp     printf

 .cfi_endproc

.LFE22:

 .size   FunNoVolatile, .-FunNoVolatile

 .ident  "GCC: (GNU) 8.3.1 20190507 (Red Hat 8.3.1-4)"

 .section .note.GNU-stack,"",@progbits

使用volatile关键字:

 .file   "example.c"

 .intel_syntax noprefix

 .text

 .section .rodata.str1.1,"aMS",@progbits,1

.LC0:

 .string "volatile: \n a + b = %d \n"

.LC1:

 .string "volatile: \n c+d = %d \n"

 .text

 .p2align 4,,15

 .globl  FunWithVolatile

 .type   FunWithVolatile, @function

FunWithVolatile:

.LFB22:

 .cfi_startproc

 sub     rsp, 24

 .cfi_def_cfa_offset 32

mov     edi, OFFSET FLAT:.LC0

mov     DWORD PTR [rsp], 10

mov     DWORD PTR [rsp+4], 100

mov     DWORD PTR [rsp+8], 0

mov     DWORD PTR [rsp+12], 0

mov     esi, DWORD PTR [rsp]

mov     eax, DWORD PTR [rsp+4]

add     esi, eax

xor     eax, eax

call    printf

mov     eax, DWORD PTR [rsp+4]

mov     edi, OFFSET FLAT:.LC1

mov     DWORD PTR [rsp], eax

mov     eax, DWORD PTR [rsp+4]

mov     DWORD PTR [rsp+8], eax

mov     eax, DWORD PTR [rsp+4]

mov     DWORD PTR [rsp+12], eax

mov     esi, DWORD PTR [rsp+8]

mov     eax, DWORD PTR [rsp+12]

add     rsp, 24

 .cfi_def_cfa_offset 8

add    esi, eax

 xor     eax, eax

 jmp     printf

 .cfi_endproc

.LFE22:

 .size   FunWithVolatile, .-FunWithVolatile

 .ident  "GCC: (GNU) 8.3.1 20190507 (Red Hat 8.3.1-4)"

 .section .note.GNU-stack,"",@progbits

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐