理解编译器无法优化的情况
在本节中,我们将讨论编译器无法应用上一节所述某些优化技术的不同场景。理解编译器何时无法优化,有助于我们编写能够避免这些失败的 C++ 代码,从而让编译器生成高度优化、高效的机器代码。
跨模块优化失败
当编译器编译整个程序时,它是按文件逐个模块独立编译的,因此无法获取当前正在编译的模块之外的函数信息。这限制了跨模块的函数优化能力,导致很多优化技术无法应用,因为编译器无法理解整个程序的上下文。
现代编译器通过 LTO(链接时优化,Link Time Optimization) 解决这一问题:在各模块独立编译后,链接器会将这些模块视作同一个编译单元,从而启用我们之前讲过的各种优化。因此,在需要优化整个应用程序时,启用 LTO 是非常重要的。
动态内存分配
我们已经知道,动态内存分配在运行时速度较慢,还会引入不可预测的延迟。但它还有一个副作用 —— 指针别名(pointer aliasing)。
动态分配的内存块常常导致编译器无法确认多个指针是否指向不同、不重叠的内存区域,哪怕对程序员来说看起来显而易见。这限制了依赖内存对齐或不重叠访问假设的各种优化。
相比之下,局部变量和局部声明 更能被频繁复用,在调用新函数或创建局部对象时更易被缓存,因此更加高效。而动态分配的内存可能随机散布在内存中,缓存性能更差。
指针别名(Pointer Aliasing)
当通过指针或引用访问变量时,即使程序员知道它们指向不同的内存位置,编译器也不能保证这一点。换句话说,编译器无法确定指针是否指向同一个变量或有重叠的内存区域。
因为必须假设有重叠的可能性,很多优化技术就不能安全地应用。
应对指针别名的建议:
在函数声明中使用
__restrict
关键字修饰指针,告诉编译器这些指针不会别名(即互不重叠)。如需进一步优化,应在了解指针别名影响的前提下手动优化代码路径。
最后,可以告诉编译器整个程序都不发生指针别名(通过编译选项),但这是非常危险的做法,应作为最后手段。
浮点归纳变量
对于浮点表达式,编译器通常不会使用归纳变量优化,因为浮点数存在舍入误差和精度问题。这会阻碍对浮点数相关计算的优化。
虽然可以开启不安全的浮点数优化编译选项,但开发者需要确保这些优化不会引发精度问题或副作用。这并非易事,因此建议开发者亲自对浮点表达式进行优化,或深入分析优化带来的副作用。
虚函数与函数指针
对于虚函数和函数指针,编译器无法在编译阶段确定将调用哪个函数,因此不能进行相应优化。
我们之前提到的 去虚化(Devirtualization) 技术可以解决一部分问题,例如当某个虚函数只有唯一的派生类实现,或对象类型在编译时可确定时。但大多数情况下,虚函数和函数指针仍然限制了优化的可能性。
系统当前共有 426 篇文章