1.简介
关于这个问题的讨论,其实是在工作中遇到了相关情况,特地写了一个demo来佐证一下自己的想法以及提出一些解决方法。
在编写代码或者针对OS进行优化时,GCC的相关优化选项是我们经常使用到的,毕竟通过编译器来进行代码优化比人力来说,还是挺香的。但这种机器优化行为,一方面,有可能破坏了我们对原本程序的设计流程,导致最终结果大相径庭。另一方面,也考验着我们的coding能力和对编译器的了解。下面就通过以下demo来说明一下。
2. 验证demo
/*************************************************************************
> File Name: test.c
> Author:
> Mail:
> Created Time: Thu 19 May 2022 10:44:40 AM CST
************************************************************************/
#include<stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
static void *thread_start(void *arg)
{
int *flag = (int *)arg;
*flag = 1;
}
int main()
{
pthread_t thread_id;
int flag = 0;
int *retval;
pthread_create(&thread_id, NULL, &thread_start, &flag);
while(!flag);
pthread_join(thread_id, (void *)&retval);
printf("Hello world\n");
return 0;
}
从代码逻辑来看,这个验证demo实现的功能很简单,就是在主进程里判断一个flag,直到这个flag被创建的线程置1了,才退出循环,并且打印一句Hello world,但使用不同的GCC编译命令,却产生了不同的结果,编译命令如下:
gcc test.c -O0 -o test_O0 -lpthread
gcc test.c -O1 -o test_O1 -lpthread
对编译出来的test_O0和test_O1,执行结果大相径庭,test_O0可以顺利打印出Hello world,而test_O1却一直阻塞在while循环那里了。
对比一下汇编代码差异,如下图所示:
很明显,使用O0编译选项编译的源码,整个循环中流程如下,
- 从内存里把flag的值加载到eax寄存器
- 判断eax寄存器的值是否为0
- 如果为0,则跳转到0x40074f这个地址重新加载内存的值到eax寄存器,再回到步骤1,周而复始,直到条件不满足。
而O1编译选项下,整个循环流程如下:
- 从内存里把flag的值加载到eax寄存器
- 判断eax寄存器的值是否为0
- 如果为0,回到步骤2,周而复始,直到条件不满足。
使用O1优化,导致eax寄存器的值没有从内存更新,只读取了一次,所以while循环条件永远无法满足,导致while循环无法退出。
3.解决方法探讨
使用volatile关键字修饰循环判断变量,当一个变量被声明为volatile的,就是告诉编译器,即便当前编译的代码不会修改这个变量,但该变量对应的内存数据也有可能因为其他原因而被修改。这样编译器在生成汇编代码时,每次使用该变量时,都会对该变量所在的内存位置进行一次访问,确保获取到内存中最新的值。如果没有加上volatile,编译器为了效率,可能就把该变量代码的值加载到寄存器中,后续需要使用时,就从相应的寄存器读取即可,不会从内存读取,即便内存数据被修改了。
是否有其他方法,待讨论…