欢迎大家来到IT世界,在知识的湖畔探索吧!
编译
学习汇编语言的主要好处不在于用它来编写底层的汇编程序,而便于我们更好的理解代码编译执行过程中发生的事情以及其对性能的影响。
确实有会存在一些比较少见的情况,需要切换到手写汇编以获得最大性能,但大多数时候,编译器能够自己生成接近最优的代码。当它们做不到这一点时,通常是因为程序员对问题的了解超出了源代码所能推断的范围,但未能将这些额外信息传达给编译器。
在这一部分中,我们将讨论如何让编译器准确执行我们想要的操作,并收集有助于进一步优化的有用信息。
编译的几个阶段(Stages of Compilation)
在直接跳转到编译器优化(这一部分的主题)之前,让我们先简要回顾一下“大局”。跳过一些相对枯燥的部分,C程序转换为可执行文件大致分为四个阶段:
- 预处理:展开宏,从头文件中提取包含的源代码,并从源代码中去除注释等。例如:gcc -E source.c(将预处理的源输出到标准输出)。
- 编译:解析源代码,检查语法错误,将其转换为中间表示,进行优化,最后翻译成汇编语言。例如:gcc -S file.c(生成.s文件)。
- 汇编:将汇编语言转换为机器代码,但是像**printf这样的外部函数调用被替换为占位符。例如:gcc -c file.c**(生成.o文件,即目标文件)。
- 链接:最终通过插入其实际地址来解析函数调用,并产生可执行二进制文件。例如:gcc -o binary file.c。
在这些阶段中,每个阶段都有提高程序性能的可能性。
跨过程优化(Interprocedural Optimization)
最后一个阶段,链接,因为按文件逐个编译程序然后将这些文件链接在一起既容易又快速——这样我们可以并行执行这些任务,并且还可以缓存中间结果。
这也提供了将代码作为库分发的能力,可以是静态的或共享的:
- 静态库简单地是预编译对象文件的集合,这些文件由编译器与其他源文件合并以生成单一可执行文件,就像它通常会做的那样。
- 动态或共享库是具有关于其可调用内容位置的额外元信息的预编译可执行文件,这些引用在运行时被解析。顾名思义,这允许在多个程序之间共享编译后的二进制文件。
使用静态库的主要优点是你可以执行各种跨过程优化,这些优化需要比库函数签名更多的上下文,例如函数内联或死代码消除。要强制链接器寻找并只接受静态库,你可以传递-static选项。
这个过程被称为链接时优化(LTO),因为现代编译器也在对象文件中存储某种形式的中间表示,这使它们能够对整个程序执行某些轻量级优化。这也允许在同一程序中使用不同的编译语言,如果它们的编译器使用相同的中间表示,甚至可以跨语言边界进行优化。
LTO是一个相对较新的功能(大约在2014年左右出现在GCC中),并且它还远未完善。在C和C++中,确保由于单独编译而不会损失性能的方法是创建仅头文件的库。顾名思义,它们只是包含所有函数完整定义的头文件,因此只需简单地包含它们,编译器就可以访问所有可能的优化。尽管你确实需要每次从头开始重新编译库代码,这种方法保留了完全控制,并确保不会损失任何性能。
检查输出(Inspecting the Output)
检查这些阶段的输出可以为你的程序中发生的事情提供有用的见解。
你可以通过向编译器传递-S标志来从源代码获取汇编,这将生成一个可读的*.s文件。如果你传递-fverbose-asm,这个文件还将包含编译器关于源代码行号和一些关于正在使用的变量的信息的注释。如果它只是一个小片段,并且你比较懒,你可以使用Compiler Explorer,这是一个非常方便的在线工具,可以将源代码转换为汇编,用不同的颜色高亮显示逻辑asm块,包含一个小型x86指令集参考,并且还有大量其他编译器、目标和语言的选择。
除了汇编之外,另一个最有帮助的抽象级别是编译器执行优化的中间表示。IR定义了计算本身的流程,与寄存器数量或特定指令集等体系结构特性的依赖性要小得多。检查这些通常有助于了解编译器如何看待你的程序,但这有点超出了本书的范围。
在本系列中,我们主要使用GCC编译器,但也会在必要时用Clang演示示例。这两个编译器在很大程度上是相互兼容的,大部分只在一些优化标志和小的语法细节上有所不同。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/76242.html