gcc/C++:如果 CPU 负载低,那么代码优化就没啥用了,对吗?
Posted
技术标签:
【中文标题】gcc/C++:如果 CPU 负载低,那么代码优化就没啥用了,对吗?【英文标题】:gcc/C++: If CPU load is low, then code optimization is of little use, true?gcc/C++:如果 CPU 负载低,那么代码优化就没什么用了,对吗? 【发布时间】:2016-01-08 12:16:21 【问题描述】:我的同事喜欢使用带有“-g -O0”的 gcc 来构建生产二进制文件,因为如果发生核心转储,调试很容易。他说没有必要使用编译器优化或调整代码,因为他发现生产中的进程没有高 CPU 负载,例如30%左右。
我问他背后的原因,他告诉我:如果CPU负载不高,瓶颈一定不是我们的代码性能,应该是一些IO(磁盘/网络)。因此,使用 gcc -O2 对提高延迟和吞吐量毫无用处。这也表明我们在代码中没有太多需要改进的地方,因为 CPU 不是瓶颈。对吗?
【问题讨论】:
你试过-O2
二进制文件运行得更快吗?
是单核的“30%”利用率,还是运行程序的系统的一般 CPU 利用率?
如果不是热点,不要优化。生活的丑陋真实(不仅在编程中有用)
您不是计算机用户空间中唯一的公民。体贴。此外,出于安全原因,如果没有别的原因,应该在生产系统上禁用核心转储...
为什么要在没有优化的情况下使用 C++?还有其他不错的语言,例如 Java 和 Python。
【参考方案1】:
关于CPU使用~优化
我预计程序中的大多数优化问题都与高于通常的 CPU 负载相关,因为我们说次优程序做比它理论上需要的更多。但是这里的“通常”是一个复杂的词。我认为您无法选择系统范围 CPU 负载百分比的硬值,在该值下优化变得有用。
如果我的程序在循环中重新分配 char
缓冲区,当它不需要时,我的程序运行速度可能比它需要的慢十倍,我的 CPU 使用率可能比它需要的高十倍,优化功能可能会使应用程序性能提高十倍……但 CPU 使用率可能仍然只有整个系统容量的 0.5%。
即使我们要选择开始分析和优化的 CPU 负载阈值,但在通用服务器上,我会说 30% 太高了。但这取决于系统,因为如果您正在为仅运行您的程序的嵌入式设备编程,并且因为它有足够的功率来运行您的程序而被选中和购买,那么 30% 可能相对较低事情的安排。
此外,并非所有优化问题确实与高于通常的 CPU 负载有关。也许您只是在sleep
中等待的时间比实际需要的时间长,这导致消息延迟增加但显着减少 CPU 使用率。
tl;dr:您同事的观点过于简单化,可能与现实不符。
关于构建优化级别
不过,就您问题的真正症结而言,在所有编译器优化关闭的情况下部署发布版本是相当不寻常的。编译器的设计目的是在-O0
发出非常幼稚的代码,并在-O1
和-O2
进行2016 年几乎“标准”的那种优化。通常希望您将它们打开以供生产使用,否则您将浪费现代编译器的大部分功能。
许多人也倾向于不在发布版本中使用-g
,以便部署的二进制文件更小,更容易让您的客户处理。通过这样做,您可以将 45MB 的可执行文件减少到 1MB,这不是零花钱。
这会使调试变得更加困难吗?是的,它可以。通常,如果发现了错误,您希望接收重现步骤,然后您可以在应用程序的调试友好版本中重复这些步骤,并分析由此产生的堆栈跟踪。
但如果该错误不能按需重现,或者只能在发布版本中重现,那么您可能会遇到问题。因此,在 (-O1
) 上保留基本优化但在 (-g
) 中保留调试符号似乎是合理的;优化本身不应极大地妨碍您分析客户提供的核心转储的能力,并且调试符号将允许您将信息与源代码相关联。
话虽如此,您也可以吃蛋糕:
使用-O2 -g
构建您的应用程序
复制生成的二进制文件
在其中一个副本上执行strip
,以删除调试符号;否则二进制文件将是相同的
永久保存它们
部署剥离版本
当您有要分析的核心转储时,根据您的原始非剥离版本对其进行调试
您还应该在您的应用程序中有足够的日志记录,以便能够在不需要任何这些的情况下追踪大多数错误。
【讨论】:
在发布版本中处理调试信息的“正确”方法是使用-g
然后strip
用于部署的可执行文件进行构建。这样可以更加确定调试信息将匹配。
@skyking:确实如此。在实践中,这两种方法应该产生相同的结果,因为具有相同配置的两个连续构建应该产生相同的二进制文件。然而,这并不严格,如果你的程序有 UB(记住我们在这个场景中追逐一个错误),它甚至可能不是真的。我会将您的建议整合到我的答案中。
人们会这样认为,但你不会得到不同的二进制文件。当然,在代码中包含某种时间戳或类似的情况。我遇到了在代码生成中不确定的编译器(这是完全有效的行为,但可能出乎意料)。【参考方案2】:
在某些情况下他可能是正确的,而在其他情况下大部分情况下是不正确的(而在某些情况下他是完全正确的)。
如果您假设您运行 1 秒,那么 CPU 将忙碌 0.3 秒并等待其他 0.7 秒。如果你优化了代码并说得到了 100% 的改进,那么 CPU 将在 0.15 秒内完成 0.3 秒的任务,并在 0.85 秒而不是 1 秒内完成任务(假设等待其他东西需要同样的时间)。
但是,如果您遇到多核情况,CPU 负载有时被定义为正在使用的处理能力。因此,如果一个内核以 100% 运行而两个内核空闲,则 CPU 负载将变为 33%,因此在这种情况下,30% 的 CPU 负载可能是由于程序只能使用一个内核。在这种情况下,如果对代码进行优化,它可以显着提高性能。
请注意,有时被认为是优化的东西实际上是一种悲观化 - 这就是衡量它很重要的原因。我已经看到了一些降低性能的“优化”。有时优化会改变行为(特别是当你“改进”源代码时),所以你应该通过适当的测试来确保它不会破坏任何东西。在进行性能测量后,您应该决定是否值得以可调试性换取速度。
【讨论】:
除非您正在编写计算密集型算法,否则优化级别变得越来越不重要,因为大部分处理依赖于预编译库。【参考方案3】:一个可能的改进可能是使用最近的 GCC 编译 gcc -Og -g
。 -Og
优化对调试器友好。
另外,你可以用gcc -O1 -g
编译;你会得到很多(简单的)优化,所以性能通常是-O2
的 90%(当然也有一些例外,甚至-O3
也很重要)。 core
转储通常是可调试的。
这实际上取决于软件的种类以及所需的可靠性和调试的容易程度。数字代码 (HPC) 与小型数据库后处理有很大不同。
最后,使用-g3
代替-g
可能会有所帮助(例如gcc -Wall -O1 -g3
)
BTW 同步问题和死锁可能更容易出现在优化代码上而不是未优化代码上。
【讨论】:
在调试数值型HPC代码的时候,很遗憾gdb还是缺少--sacrifice=goat
选项。
不应该是--burn=dead-cat
吗? :-) :-)【参考方案4】:
这很简单:CPU 时间不是空闲的。我们喜欢认为它是,但它显然是错误的。在某些情况下,有各种各样的放大效果使每个循环都很重要。
假设您开发了一个可在数百万台移动设备上运行的应用。您的代码浪费的每一秒都相当于在 4 核设备上 连续 使用 1-2 年的设备价值。即使在 CPU 利用率为 0% 的情况下,挂墙时间延迟也会消耗您的背光时间,这也不容忽视:背光消耗了大约 30% 的设备功率。
假设您开发了一个在数据中心运行的应用。您正在使用的每 10% 的核心都是其他人不会使用的。归根结底,一台服务器上只有这么多内核,而且该服务器有电力、冷却、维护和摊销成本。每 1% 的 CPU 使用量都有很容易确定的成本,而且它们不是零!
另一方面:开发人员的时间不是免费的,开发人员每一秒的注意力都需要相应的精力和资源投入,才能让她或他保持活力、吃饱、健康和快乐。然而,在这种情况下,开发人员需要做的就是翻转编译器开关。我个人不相信“更容易调试”的神话。现代调试信息的表现力足以捕捉寄存器使用、价值活力、代码复制等。优化并没有像 15 年前那样真正妨碍您。
如果您的企业有一个未充分利用的服务器,那么开发人员所做的工作实际上可能没问题。但我在这里看到的真的是不愿意学习如何使用调试工具或适当的工具。
【讨论】:
当真正重要时,优化可能是 PITA。缺乏优化可能会留下对事后分析有用的线索,汇编代码可能更容易理解。 @skyking 鉴于优化通常会导致更少的输出代码(缓存压力!),并且优化过程可以更好地检测并遵循源代码中的模式,我实际上发现优化的程序集更易于阅读。 有许多优化可以使程序集更难阅读。指令调度通常会导致汇编程序不能很好地遵循源代码。函数之间的代码共享也会使代码更难遵循。 @skyking 代码共享使您只需了解一次给定的汇编代码。就个人而言,我喜欢它。在我的书中,任何减少最终可执行程序的汇编数量的东西都非常好——无论是对于 CPU,还是对于人类读者。 但它根本不遵循源代码。顺便说一句,我没有读过你的书,我是根据自己的经验来借鉴的。以上是关于gcc/C++:如果 CPU 负载低,那么代码优化就没啥用了,对吗?的主要内容,如果未能解决你的问题,请参考以下文章