在《Effective C++》一书中,Scott Meyers 谈到了使用 lhs 和 rhs 作为参数名的方式:“……这是我最喜欢的两个参数名。但在你没有接触过编译器编写工作的情况下,它们的优势和含义可能并不突出。”
大约在 1992 年,当 Scott 写这篇文章时,他一定是联想到了 GCC,因为当时 Clang/LLVM 还没出现。Clang/LLVM 从根本上改变了人们对编译器的思考方式,揭开了手动编译器制作的神秘面纱。点击链接,阅读更多什么是 Clang 以及GCC vs Clang 的内容。
在这篇博客中,我想说明的是,你不需要接触编译器编写工作就可以理解Clang 的优化方式。我希望能解释 Clang 优化Flag的原理,帮助大家充分利用这个功能,并学会使用不同的 Clang 优化Flag。
这篇文章将在 Windows 环境中使用 Clang(Clang 支持 Windows 编译,前面推荐的博客中已进行了详细解释)。然而,在本篇博客中,我们并没有特别针对 Windows 系统,而是聚焦 Clang 优化功能,并阅读一些汇编语言,这些语言也同样适用于 Linux 系统。所以,如果你是一个 Linux C++ 程序员,请继续阅读,因为这个帖子也适合你。
在我尝试破译 Clang 优化标志之前,请注意一点,Clang/LLVM 是一个非常活跃的项目。我正在研究 2021 年 4 月 15 日发布的最新 Clang/LLVM 版本,但从这次发布以来,主机上显示已有 12228 次提交,所以我担心我写的内容可能很快就会过时。
C:\>clang --version
clang version 12.0.0
Target: x86_64-pc-windows-msvc
Thread model: posix
InstalledDir: C:\Program Files\LLVM\bin
首先,我将继续采用我在如何避免 C++ 编译失败博客中使用的案例,让大家更好地理解优化标志。
void ConvertStringToPasswordForm(char password[])
{
while (*password != '\0') *password++ = '*';
}
以及 driver:
nt main()
{
char password[] = "MyTopSecurePasswordPublishedInABlog:-)";
ConvertStringToPasswordForm(password);
std::cout << "Password :: " << password << std::endl;
}
如果我们使用 Clang 编译器运行下列命令:
C:\Work\Temp>clang Example1.cpp
默认情况下,Clang 编译器会静默地进行编译,并创建一个可执行的 a.exe 文件。 接下来,我们简要对比一下 Clang 和Microsoft C++ 编译器(Cl) 的行为。
Clang:
C:\Work\Temp>clang Example1.cpp
输出: a.exe
大小: 244,224 bytes
Microsoft C++ compiler (Cl)
C:\Work\Temp>cl Example1.cpp
Microsoft (R) C/C++ Optimizing Compiler Version 19.27.29111 for x86
Copyright (C) Microsoft Corporation. All rights reserved.
Example1.cpp
C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.27.29110\include\ostream(747): warning C4530: C++ exception handler used, but unwind semantics are not enabled. Specify /EHsc
Example1.cpp(12): note: see reference to function template instantiation 'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,const char *)' being compiled
Microsoft (R) Incremental Linker Version 14.27.29111.0
Copyright (C) Microsoft Corporation. All rights reserved.
/out:Example1.exe
Example1.obj
输出: Example.exe
大小: 186,368 byte
Microsoft 编译器清晰完整地展示了使用的编译器和链接器版本信息,并生成较小的可执行文件。问题是:应该将哪些标志传递给 Clang,使其空间优化与 Cl 相当甚至超过 Cl?
在回答这个问题之前,让我们先看一下文档信息,其中讨论了 Clang 标志的代码生成选项:https://clang.llvm.org/docs/CommandGuide/clang.html。为了便于讨论,我复制了以下信息:
有了这些信息,现在让我们从 -O1 开始进行空间优化。
空间优化
使用 -O1 标志运行 Clang:
clang -O1 Example1.cpp
为 a.exe 提供 236032 字节,可执行文件的大小有一定的减少。默认的 Clang 标志为 -O0,它生成了一些未优化的代码。
如果你将获得的可执行文件的二进制文件与 -O0 和 -O1 标志进行比较,你将看到一些差异,但你无法找出这些差异的原因。我们启用了哪些优化?为此,我们来看一下使用标志 -O0 和 -O1 生成的代码汇编列表。我们通过命令 -O0 和 -O1 生成汇编代码列表。
clang -S -O1 -mllvm --x86-asm-syntax=intel Example1.cpp
clang -S -mllvm --x86-asm-syntax=intel Example1.cpp
# — Begin function and # -End函数之间的汇编代码列表,清楚地展示了对ConvertStringToPasswordForm 所做的优化
接下来列出我们观察到的汇编代码的差异:
1\O1 标志无法生成 .seh_proc、 .seh_stackalloc、.seh_endprologue 和 .seh_endproc 函数。对于带有 -O1 标志的函数ConvertStringToPasswordForm,结构化异常处理已完全关闭。
2\ 使用 -O1 标志生成了紧密的循环,从而减少了空间:
在使用 -O0 标志生成的上下文中,我们应该可以看到:
这里的重点是:
1\标签数量减少。
2\使用了 lea(加载有效地址)和 jne(跳转不等于)等高效指令
3\如果 eax 为 0,比较和跳出循环以标记 LBB0_3 等步骤已完全跳过。
注意:
我在 example1.cpp 上进一步试验了 Clang 优化标志 -O2 和 -Os。以下是空间缩减的列表,供大家快速对比:
-O0 244,224 bytes
-O1 236,032 bytes
-O2 233,984 bytes
-Os 231,424 bytes
-Oz 229,376 bytes
可以看出,在从 -O0(无优化)到 -Oz(积极的空间优化)的过程中,可执行文件大小逐渐减小。尽管我没有对其进行测量,但可以确定的是,在这些阶段,编译时间也在逐渐增加。
深入分析
如果没有实际去操作,可能很难分析汇编代码。阅读(而不是编写)汇编代码是我真心推荐给所有开发人员的一项学习技能。请放心,Clang/LLVM 有一个开关,描述了编译运行期间使用的具体优化:
clang -O3 -foptimization-record-file=Opt.txt Example1.cpp
Opt.txt 文件将包含所有优化的详细信息。你将获得如下条目:
--- !Analysis
Pass: prologepilog
Name: StackSize
DebugLoc: { File: Example1.cpp, Line: 3, Column: 0 }
Function: '?ConvertStringToPasswordForm@@YAXQEAD@Z'
Args:
- NumStackBytes: '0'
- String: ' stack bytes in function'
...
在 LLVM 中,实现优化是通过程序的某些部分来收集信息或转换程序的过程。在上述条目中,通行证名称为“prologepilog”。你可以从线上的参考资料中获得不同编译器开关的完整信息:
https://clang.llvm.org/docs/ClangCommandLineReference.html .
你还可以运行 clang–help 或 clang–help hidden 获得联机帮助。有一个隐藏的帮助功能,介绍了所有可用的高级开关!
Clang 优化标志,我们还没完成!
Clang 的核心是 LLVM。因此如果不介绍如何使用 LLVM 中间语言, Clang 优化标志的文章是不完整的。以下是如何获取中间语言字节码的方式:
clang -c -O1 -emit-llvm Example1.cpp -o Example.bc
一般来说,LLVM 字节码文件的扩展名为 .bc。要进一步使用字节码文件,你需要借助一些工具,这些工具是 Clang/LLVM 安装程序没有提供的。
首先,点击链接下载 LLVM 源代码。提取源代码并存储到名为 llvm-project-llvmorg-12.0.0 的文件夹中。在 llvm-project-llvmorg-12.0.0\llvm 下创建一个名为 build 的文件夹,这个步骤需要提前安装 python。
现在你可以使用 CMake 了。如果你还不了解什么是 CMake ,请阅读我的博客。
下面,我将展示 LLVM tools 文件夹中一个名为 opt 的工具。要从源代码处编译此文件,请使用以下命令:
cd build
cmake .. -DLLVM_TARGETS_TO_BUILD=X86
cmake --build . -t opt
记住,这不是一个快速构建。在获得 opt.exe 最终工件之前,需要构建 92 个依赖库。你可以利用 opt.exe 打印帮助,并且可以查看 LLVM 支持的所有优化。以下是你将获得的部分信息:
总结
文章即将结束,我需要反思一下,是否本文已经达到了我设定的目标,大家理解了 Clang 优化标志的用法了吗?我相信我已经说清楚了。Clang/LLVM 并不是一个趣味性工具,而是有其实际的功能。理解开发中的基本工具——编译器——及其行为,对开发新手的成长来说至关重要。作为程序员,你需要了解改变编译输出的 Clang 编译器标志。当然,如果你已经编写了 LLVM 优化过程,而且开始使用 LLVM 进行静态代码分析,或者对全局值编号(Global Value Numbering)有了深入地了解,那么你已经是大师级的程序员了!我也为你的成绩感到骄傲!
《C++编译加速指南》白皮书点击此处下载