Clang 比 GCC 好在哪里?


作者:Incredibuild 链接:https://www.zhihu.com/question/20235742/answer/3105870528 来源:知乎

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

输出: Example.exe 大小: 186,368 byte

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

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

img

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

空间优化

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

img

如果你将获得的可执行文件的二进制文件与 -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 标志仅运行预处理和编译步骤。

img

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

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

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

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

img

这里的重点是:

  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下创建一个名为 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 支持的所有优化。以下是你将获得的部分信息:

img

总结

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


文章作者: sfc9982
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 sfc9982 !
  目录