尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

探秘 volatile关键字:编译器的过度优化

探秘 volatile关键字:编译器的过度优化 探秘volatile编译器的过度优化信号相关内容可以看这里Linux信号机制博客1. 一个诡异的现象加了优化程序就不跑了作为一名 C/C 开发者你一定遇到过这样的场景代码逻辑明明是对的Debug 版本跑得欢天喜地一上 Release 版本开启优化就直接“罢工”了。今天我们要复现一个经典案例。我们先写一段非常简单的代码test.c第一步不加优化编译此时按下CtrlC完美运行信号捕捉将 flag 改为 0循环退出。第二步加上优化编译-O2此时疯狂按下CtrlC诡异的事情发生了明明信号处理函数执行了flag也被改成了 0为什么while循环就是不退出去2. 刨根问底谁“偷走”了我的内存访问如果代码逻辑没变那变的一定是编译器的行为。这一切的幕后黑手就是编译器的优化策略。我们来看while (flag)这行代码。在计算机内部这行代码对应以下 CPU 指令周期Load加载从内存RAM读取flag的值。Compare比较将读取的值与 0 比较。Jump跳转如果不等于 0跳回循环开始。编译器在开启-O2优化时会进行常量传播和寄存器缓存优化。编译器会想“main函数里没有任何地方修改flag那它就是个只读变量。既然如此我何必每次都费力去内存里读数据呢内存访问速度比寄存器慢几十倍我把flag的值也就是 1直接加载到 CPU 的寄存器如%eax中以后每次判断直接用寄存器里的值不香吗”于是优化后的汇编伪代码变成了关键时刻来了我们按下CtrlC操作系统发出信号调用signal_handler。这个函数确实修改了内存地址中的flag为 0。但由于寄存器的缓存副本并没有被更新CPU 在循环判断时根本不去看内存而是死盯着寄存器里那个过时的值1。所以即便内存里的flag已经变成了 0CPU 依然认为它是 1进程永远无法退出。3. 破局之钥volatile关键字既然编译器“好心办坏事”了我们就必须通过语法来告诉编译器“别耍小聪明别给我优化这个变量”这就是volatile关键字的用武之地。它的本意是“易变的”用来修饰那些可能被程序流程之外的因素如硬件、中断、信号、其他线程意外修改的变量。我们只需在定义时加上volatilevolatileintflag1;// 加上 volatile 修饰加上volatile后发生了什么编译器在看到volatile时会生成一条特殊指令前缀强制要求 CPU每次都从内存地址重新读取数据到寄存器绝不使用寄存器中的历史缓存值。重新编译运行后无论开启多么激进的优化-O3只要信号到来修改了内存中的flagwhile循环的下一轮判断就会乖乖去内存读取拿到最新的0循环正常退出。4. 双生子对比被时代淘汰的register聊到这里就不得不提它的“反义词” ——register关键字。volatile“别优化我别把我放寄存器每次都去读内存”register“兄弟尽量把我放在寄存器里别在内存里占地方了”在几十年前编译器优化技术还很“愚蠢”时程序员经常使用register int i;来建议编译器将高频使用的循环变量放在寄存器中以提高效率。但现在的情况是C17 标准已经正式将register关键字废弃Deprecated并移除。C 语言标准虽然保留但也明确表示编译器可以无视它。为什么因为现代编译器的寄存器分配算法图着色算法远比程序员肉眼观察要精准。你手动加了register反而可能挤压了其他更重要的变量导致性能下降。总结一句经典的话register劝变量进寄存器被编译器无视且已淘汰volatile禁变量进寄存器被编译器强制执行且很重要。结语volatile是一个专为底层硬件访问、中断服务程序ISR和信号处理而生的“上古神兵”。在应用层业务开发中它几乎绝迹但在 Linux 驱动、STM32 单片机、RTOS 实时操作系统中它是必备的刚需。一句话总结本文当编译器优化让你的程序“指东打西”时别忘了volatile这个“死命令”它能让 CPU 每次乖乖下基层去内存查看真实情况。希望这篇博客能帮你彻底讲透volatile
返回列表