上次我们说到现代编译器已经非常聪明,为了保证程序的执行效率,会在编译时对代码做优化。水平较低的程序员写出的代码比较臃肿,编译器的优化确能够增加程序的执行效率。但是,编译器有时“聪明过了头”,自以为是的把有用的语句优化掉了,反而导致程序不能正常工作。
例如下面这几句代码,
int a;
a = 0x10; // 10000
a = 0x08; // 01000
编译器很聪明,会认为 “a = 0x10” 这句没有意义,因为我们并没有使用 a 的 0x10 这个值,在下一句就被 0x08 覆盖了。所以在编译时,编译器直接就忽略了 “a = 0x10” 这句,这样的确会增加效率,但是在某些情况下,即使只是赋值,也是有意义的。
还记得我们在《程序员编写的代码为什么可以控制计算机硬件工作?》一节中提到的电灯开关的例子吗?我们用 0 表示灭灯,用 1 表示开灯。
上图的电灯状态显然可以用 01000 表示。
现在假设我们用变量 a 表示电灯的状态,那么“a = 0x10” 这句就表示开最左边的灯,它是有意义的。
本来电灯管理员想先开一下最左边的灯,看看它亮不亮,有没有坏掉,那现在编译器把这句忽略了,最左边的灯即使没坏也不亮,会害的电灯管理员花很多时间检查电路。
这就是编译器“自作聪明”带来的问题。那我只能不让编译器做优化了吗?答案是没有必要因为这点小问题,就全盘否定编译器优化带来的效率提升。C99 提供了 "volatile"
关键字,就是专门用来解决这样的问题的。
"volatile"
的字面意思是“不稳定的,易变的”,它可以用来修饰变量,以此来告诉编译器,别优化我。编译器看到以后,就知道下面这几行代码不需要优化,不管自己看着多么不爽,也照样执行,不做任何优化:
volatile int a;
a = 0x10; // 10000
a = 0x08; // 01000
使用了 volatile 关键字修饰代表电灯开关的变量 a,编译后,会发现最左边的电灯也亮了一下。电灯管理员知道它没坏,可以安心了。
你可能会说,我编程不涉及硬件,这个关键字用不到。可是,即使如此,编译器的优化,有时候也会让人觉得很困惑。
请看下面这段代码:main 函数一直在判断变量 is_continue
的值是否变为 0,如果变了,就在终端打印出 “hello world”,否则就一直等下去。thread
函数在一秒后,将is_continue
的值修改为 0。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int is_continue = 1;
void* thread(void* p)
{
sleep(1);
is_continue = 0;
return NULL;
}
int main()
{
pthread_t ppid;
pthread_create(&ppid, NULL, thread, NULL);
while(is_continue);
printf("hello world\n");
return 0;
}
可以看出,原本程序员的意图是通过变量 is_continue
控制 main 打印出 "hello world" 的时机(这里是1秒后打印)。我们编译它,执行会发现,程序一直不打印“hello world”,这很奇怪。我们一起来看看编译后,程序的汇编代码:
$ gcc t.c -lpthread -O2 -g
$ objdump -dS a.out
a.out: file format elf64-x86-64
...
这就明白了,编译器“自作聪明”的优化了代码,导致程序没有按照程序员的意图运行。
编译器认为:在 main 函数中,没有任何对is_continue
的修改,while() 也是什么都没做,所以只需要判断is_continue
最开始的值即可,不需要再从内存重新读取is_continue
。
接下来,我们在“int is_continue = 1;”前加上 “volatile”关键字,其他的代码都不变,再编译执行,发现 1 秒后,终端有“hello world”打印。查看相应的汇编代码:
编译器见到“volatile”关键字后,不再自作主张,每次都从内存读取 is_continue
再做判断,程序终于与预期一致了。