教学视频——浅析 C++ 优化器

Blog
Author:
Yang ChrisYang Chris
Published On:
12月 27, 2021
Estimated reading time:
1 minute

编译器优化是通过改进运行时性能或最小化代码大小来提高已编译代码效率的过程。

在本视频中(转录文本见下文),Incredibuild 的开发推广工程师 Amir Kirsh 将重点讨论 C++ 编译器执行的一些特定的和有趣的优化。

 

1-浅析 C++ 编译器

c++ 优化器是如何优化代码的呢?

让我们深入研究一段代码。我将和大家分享我的 Coliru 在线编译器。

无锁多线程

在这个例子中,我给出了一个多线程程序,为什么向您展示多线程程序呢,我很快会为您解释原因。这里有一个叫做加法器的函数,它可以将数字添加到全局变量和中。在这个程序中,全局变量和是由多个线程并行处理的,并没有锁定,这一点并不好。这是众所周知的问题。我们有一个竞争环境,结果可以是任何东西。但是这个想法的目的是检查实际结果是什么。因此,如上所述,和是一个全局变量,加法器运行一个循环,并将指数 “i” x 的乘积加入和中。总的来说,我们启动了 10 个线程。每个线程都运行加法器,且所有 10 个线程都将数字相加到同一全局变量和中。

因此,如果我们想要检查结果会是什么,可能我们只运行一次,就能得到预期的结果,我们可以计算并看看有锁时会发生什么。但是,如果我们多次运行程序,我们会发现结果确实并不总是相同的。所以我在这里做的是运行一个小的 bash 脚本。在这里,我将程序运行了 500 次,然后我对结果排序并计算唯一的结果,最终我得到了我得到每个特定输出的次数。我们可以看到,500 次中有 490 次,我得到了一些结果,这是预期的结果,这是正确的计算,如果我有锁的话。但是我们还有 10 个其他结果,则是另外一个结果。这很棒。

它确实表明,我们有理由需要在这里使用一个原子整数或使用锁。但我的问题是,为什么我们得到的所有结果都是 50 的乘积呢。为什么我们没有其他的结果,比如 267355267384、或者很多其他的结果。答案就是编译器优化。经过优化器的处理,结果成为了约为 10 的乘积或 50 的乘积。而优化器所作的这种处理就是循环展开。

循环展开

优化器看到这个循环,然后它说,好吧,我不确定你是否真的需要一个循环。也许我可以不用循环计算整个过程。所以函数加法器变成了一个单独的计算,循环没有了,这叫做循环展开。

一旦循环消失,那么线程之间的竞争,也就是竞争条件,就会出现在由优化器计算的整个计算中。循环中的和是多少,是 50 的乘积之类的。现在,我们能验证这个理论吗? 是的,我们可以。我们试着将优化标志从 -O2 改为 -O0,并重新编译。因此,我在这里再次将优化标志更改为 -O0 编译。我们可以看到,结果现在更广泛地分布在各种数字中,因为现在没有循环展开。循环实际上在代码中。所以我们实际上进入了循环,竞争条件现在是循环中的每一个加法,而不是单次计算。大多数时候,我们得到的数据与之前相同,但其他的数据更加分散。

汇编

说到汇编代码,我们可以使用另一个在线工具 Compiler Explorer。我们可以看到相同代码上的 -O0 -O2 会导致不同的汇编代码。使用 -O0,我们在这里谈到的加法器函数确实有一个循环。我们可以看到,我们转到有一些计算的代码,然后在这一行进行比较。如果比较大于某个数字,则继续,否则跳转回循环。

这就是我们看到的循环,在使用 -O2 优化的代码中,汇编代码,在加法器函数中,汇编命令只是计算,一行或两行计算,两个计算命令。就是这样。没有任何内部循环,这就是我们在这看到的循环展开。

结语

今天我们谈论了由 C++ 编译器所做的优化,也就是循环展开。在我们的示例中,我们看到了它如何影响多线程应用程序。它并没有将我们从竞争条件中拯救出来,因为无论如何竞争条件都在加法器函数中,但是如果我们没有循环展开,竞争条件的表现会有所不同。这就是今天的内容。我们将在下一次会谈中继续讨论,看看优化器能为我们做些什么。感谢大家的观看。非常感谢!再见!