gcc/C++:如果CPU负载很低,那么代码优化用处不大,这是真的

gcc/C++: If CPU load is low, then code optimization is of little use, true?

本文关键字:代码优化 真的 如果 C++ CPU 负载 gcc      更新时间:2023-10-16

我的同事喜欢使用带有"-g-O0"的gcc来构建生产二进制文件,因为如果发生核心转储,调试很容易。他说,没有必要使用编译器优化或调整代码,因为他发现生产中的进程没有很高的CPU负载,例如30%左右。

我问他背后的原因,他告诉我:如果CPU负载不高,瓶颈一定不是我们的代码性能,应该是一些IO(磁盘/网络)。因此,使用gcc-O2对提高延迟和吞吐量毫无用处。此外,这也表明我们在代码中没有太多需要改进的地方,因为CPU不是瓶颈。这是正确的吗?

关于CPU使用~优化

我预计程序中的大多数优化问题都与高于通常的CPU负载有关,因为我们说次优程序所做的比理论上需要的要多。但这里的"通常"是一个复杂的词。我不认为你可以选择一个系统范围内CPU负载百分比的硬值,在这个值下优化会变得有用。

如果我的程序在循环中重新分配一个char缓冲区,当它不需要的时候,我的程序可能会比它需要的慢十倍,我的CPU使用率可能会比需要的高十倍,优化函数可能会使应用程序性能提高十倍…但是CPU使用量可能仍然仅为整个系统容量的0.5%。

即使我们要选择一个CPU负载阈值来开始分析和优化,在通用服务器上,我也会说30%太高了。但这取决于系统,因为如果你正在为一个只运行你的程序的嵌入式设备编程,并且因为它刚好有足够的功率来运行你的节目而被选择和购买,那么30%在总体方案中可能相对较低。

此外,并非所有优化问题都与高于通常的CPU负载有关。也许您只是在sleep中等待的时间比实际需要的时间长,导致消息延迟增加,但大大降低了CPU使用率。

tl;博士:你同事的观点过于简单,可能与现实不符


关于构建优化级别

然而,关于你问题的真正症结,在关闭所有编译器优化的情况下部署发布版本是相当不寻常的。编译器被设计为在-O0中发出相当幼稚的代码,并在-O1-O2中进行2016年相当"标准"的优化。通常情况下,您需要将这些功能用于生产用途,否则您将浪费现代编译器的很大一部分功能。

许多人也倾向于不在发布版本中使用-g,这样部署的二进制文件就更小,更容易让客户处理。通过这样做,您可以将一个45MB的可执行文件降到1MB,这是不需要零钱的。

这会使调试更加困难吗?是的,可以。通常,如果找到错误,您希望接收再现步骤,然后可以在应用程序的调试友好版本中重复这些步骤,并分析由此产生的堆栈跟踪。

但是,如果bug不能按需复制,或者只能在发布版本中复制,那么您可能会遇到问题。因此,将基本优化保持在(-O1)上但也将调试符号保持在(-g)中似乎是合理的;优化本身不应该极大地阻碍您分析客户提供的核心转储的能力,调试符号将允许您将信息与源代码关联起来。

话虽如此,你可以拥有你的蛋糕,也可以吃:

  • 使用-O2 -g构建应用程序
  • 复制生成的二进制文件
  • 对其中一个副本执行strip,以删除调试符号;否则二进制文件将完全相同
  • 永远保存它们
  • 部署剥离版本
  • 当你有一个核心转储要分析时,对照你的原始非剥离版本进行调试

您还应该在应用程序中有足够的日志记录,以便能够在不需要任何这些的情况下跟踪大多数错误。

在某些情况下,他可能是正确的,在其他情况下大多是不正确的(而在某些情况中,他是完全正确的)。

如果你假设你运行了1s,CPU会忙碌0.3秒,等待其他事情0.7秒。如果你优化了代码,并说得到了100%的改进,那么CPU会在0.15秒内完成0.3秒的工作,并在0.85s而不是1s内完成任务(假设等待其他事情需要同样的时间)。

然而,如果您有多核情况,CPU负载有时被定义为正在使用的处理能力。因此,如果一个内核以100%运行,而两个内核处于空闲状态,则CPU负载将变为33%,因此在这种情况下,30%的CPU负载可能是由于程序只能使用一个内核。在这种情况下,如果对代码进行优化,它可以显著提高性能。

请注意,有时被认为是优化的东西实际上是一种悲观主义——这就是为什么测量很重要。我看到了一些降低性能的"优化"。还有一些时候,优化会改变行为(特别是当你"改进"源代码时),所以你可能应该通过适当的测试来确保它不会破坏任何东西。在进行性能测量后,您应该决定是否值得用可调试性来换取速度。

一个可能的改进可能是使用最近的GCC使用gcc -Og -g进行编译。-Og优化对调试器友好。

此外,您还可以使用gcc -O1 -g进行编译;您得到了许多(简单的)优化,所以性能通常是-O2的90%(当然也有一些例外,即使是-O3也很重要)。并且core转储通常是可调试的。

这实际上取决于软件的种类以及所需的可靠性和调试的方便性。数字代码(HPC)与小型数据库的后处理有很大不同。

最后,使用-g3而不是-g可能有助于(例如gcc -Wall -O1 -g3

BTW同步问题和死锁可能更容易出现在优化的代码上,而不是非优化的代码。

它真的很简单:CPU时间不是免费的。我们喜欢认为这是事实,但这显然是错误的。在某些场景中,有各种放大效果使每个周期都有意义。

假设你开发了一个在一百万台移动设备上运行的应用程序。在4核设备上,您的代码每浪费一秒钟就相当于连续使用设备1-2年。即使CPU利用率为0%,墙壁时间延迟也会花费背光时间,这一点也不容忽视:背光消耗了设备30%的功率。

假设您开发了一个在数据中心运行的应用程序。你使用的每10%的核心都是别人不会使用的。归根结底,一台服务器上只有这么多核心,而这台服务器需要电源、冷却、维护和摊销成本。每1%的CPU使用率都有很容易确定的成本,而且它们不是零!

另一方面:开发人员的时间不是免费的,开发人员的每一秒注意力都需要相应的精力和资源投入,才能让她或他活着、吃饱、健康和快乐。然而,在这种情况下,开发人员所需要做的就是打开编译器开关。我个人并不相信"调试更容易"的神话。现代调试信息具有足够的表达能力,可以捕捉寄存器的使用、值的活泼性、代码复制等等。优化并不像15年前那样成为阻碍。

如果您的企业只有一个未充分利用的服务器,那么实际上,开发人员所做的可能还可以。但我在这里看到的只是一开始不愿意学习如何使用调试工具或适当的工具。