使用 Clang 优化Flag进行编译

Blog
Author:
Dori ExtermanDori Exterman
Published On:
8月 12, 2021
Estimated reading time:
2 minutes

在《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:

int 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 Microsoft C++ compiler (Cl)
C:\Work\Temp>clang Example1.cpp
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<char,std::char_traits<char>> &std::operator
<<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>>
&,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
输出: a.exe 输出: Example.exe
大小: 244,224 bytes 大小: 186,368 byte

Microsoft 编译器清晰完整地展示了使用的编译器和链接器版本信息,并生成较小的可执行文件。问题是:应该将哪些标志传递给 Clang,使其空间优化与 Cl 相当甚至超过 Cl

在回答这个问题之前,让我们先看一下文档信息,其中讨论了 Clang 标志的代码生成选项:https://clang.llvm.org/docs/CommandGuide/clang.html。为了便于讨论,我复制了以下信息:

Clang-Optimization-Flags

* -O4 及更高版本– 目前相当于 -O3,更多内容:

https://clang.llvm.org/docs/CommandGuide/clang.html#code-generation-options

有了这些信息,现在让我们从 -O1 开始进行空间优化。

空间优化

使用 -O1 标志运行 Clang

clang -O1 Example1.cpp

a.exe 提供 236032 字节,可执行文件的大小有一定的减少。默认的 Clang 标志为 -O0,它生成了一些未优化的代码。

Clang-Optimization-Flags_2

如果你将获得的可执行文件的二进制文件与 -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

注意,-S 标志仅运行预处理和编译步骤。

# — Begin function and # -End函数之间的汇编代码列表,清楚地展示了对ConvertStringToPasswordForm 所做的优化。

接下来列出我们观察到的汇编代码的差异:

  1. O1 标志无法生成 .seh_proc、 .seh_stackalloc、.seh_endprologue  和 .seh_endproc 函数。对于带有 -O1 标志的函数ConvertStringToPasswordForm,结构化异常处理已完全关闭。
  2. 使用 -O1 标志生成了紧密的循环,从而减少了空间: Clang-Optimization-Flags_4

在使用 -O0 标志生成的上下文中,我们应该可以看到:

Clang-Optimization-Flags_5

这里的重点是:

  • 标签数量减少。
  • 使用了 lea(加载有效地址)和 jne(跳转不等于)等高效指令
  • 如果 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-Optimization-Flags_6

总结

文章即将结束,我需要反思一下,是否本文已经达到了我设定的目标,大家理解了 Clang 优化标志的用法了吗?我相信我已经说清楚了。Clang/LLVM 并不是一个趣味性工具,而是有其实际的功能。理解开发中的基本工具——编译器——及其行为,对开发新手的成长来说至关重要。作为程序员,你需要了解改变编译输出的 Clang 编译器标志。当然,如果你已经编写了 LLVM 优化过程,而且开始使用 LLVM 进行静态代码分析,或者对全局值编号(Global Value Numbering)有了深入地了解,那么你已经是大师级的程序员了!我也为你的成绩感到骄傲!

Whitepaper download