做者丨 Vittorio Romeo
译者丨明知山
筹谋丨杜小芳
到目前为行,“‘零成本笼统’是一个谎话”应该(希望如斯)已经成为一个常识了。公允地说,那更像是用词不妥——“笼统在颠末优化后可能供给零运行时开销”如许的说法可能会更安妥一些,但我晓得为什么不是那么回事……
大大都 C++ 法式员倾向于承受如许一个事实——“零成本笼统”只在启用了优化的情状下才气供给零运行时开销,并且它们对编译速度有负面的影响。同样是那些人,他们倾向于相信那种笼统是如斯的有价值,以致于认为让他们的法式在调试形式下施行得很差(即没有启用优化)和编译得更慢是值得的。
我曾经也是他们中的一员。
然而,在过去的几年里,我起头意识到,在某些范畴拥有高性能调试和快速编译是多么的重要,好比游戏开发。处置游戏开发的人往往直抒己见地说 C++ 的笼统与他们的工做格格不入,并且他们有足够的理由——游戏是实时模仿的,即便在调试版本中也需要可玩性和响应性——想象一下在 20FPS 摆布的帧率下调试虚拟现实游戏招致眩晕的情形。
在本文中,我们将切磋 C++ 的笼统模子若何严峻依赖编译器优化,并显示一些招致不测性能丧失的例子。之后,我们将比力三种次要编译器(GCC、Clang 和 MSVC)在那方面的表示,并讨论一些潜在的改良或处理计划。
挪动 int 很慢
我在本年的 ACCU 2022 大会上做了一场闪电演讲(“挪动 int 很慢:调试性能很重要!”),演讲的标题问题具有搬弄意味——挪动 int 怎么会很慢?
我们来看一下那段代码。
intmain{returnstd::move( 0); }
C++ 法式员应该晓得 std::move(0) 在语义上与 static_castint(0) 不异,并且大大都人都希望编译器不会为 move 生成代码,即便禁用了优化。成果是 GCC 12.2、Clang 14.0 和 MSVC v19.x 最末城市生成一个 call 指令。
你可能认为那没什么大不了的——究竟结果,那里或那里多出一个额外的 call 指令又有什么关系呢?下面是一个高性能算法的例子,它的内部轮回中包罗了一个 move。
请留意 C++ 17 及以上版本中的 init 对象在每次轮回时是若何挪动的。具有挖苦意味的是,从 C++ 14 切换到 C++ 17,因为额外的 std::move 招致利用了 std::accumulate 的法式调试性能呈现庞大的丧失——想象一下在处置算术类型对象的轮回中每次挪用无用函数的开销!
情状比想象的更糟
std::move 不是一个孤立的例子——在禁用优化的情状下,任何语义上是强逼转换的函数最末城市生成一个无用的 call 指令。那里还有一些例子——std::addressof、std::forward、std::forward_like、std::move_if_noexcept、std::as_const、std::to_fundamental。
展开全文
假设你完全不关切调试性能……好吧,猜猜怎么着——所有上述的适用函数城市招致函数模板实例化,从而降低编译速度。此外,那些“强逼转换”将在调试时做为挪用仓库的一部门呈现,使逐渐遍历代码的过程变得愈加痛苦和嘈杂。
强逼转换的适用函数并非独一一种没有优化就表示得很蹩脚的笼统类别——关于概念上的轻量级类型,如 std::vector ::iterator,没有人希望在调试时进入 iterator::operator* 和 iterator::operator++,也没有人希望在遍历 std::vector 时每次迭代都需要付出挪用函数的开销。然而,在调试形式下,情状就是如斯。
在 C++ 中,你能够在任何处所找到如许的例子。值得留意的是,下面是 Chris Green 关于 std::byte 的推文:
你实的不会想要利用 std::byte()。
你实的不会想要利用 std::byte()。
从链接的 Compiler Explorer 示例( 其实不会生成如斯蹩脚的汇编,即便完全禁用了优化。
后果是什么
那些低效率的成果关于 C++ 在游戏开发范畴的声誉和用处来说是扑灭性的,而且(在我看来)还会招致更低的消费效率和更长的调试周期。
起首,到目前为行我们所展现的一切都意味着任何开发重要项目标游戏开发者都不会利用“零成本笼统”。std::move、std::forward 等都将被强逼转换或宏替代。
不倡议利用 std::vector ,而倡议利用 T*,或者至少通过指针停止迭代(即通过 std::vector ::data),而不是通过迭代器。
来自 和 头文件的任何工具都可能不会被利用,因为有很大的开销风险(就像 std::accumulate 那样),或者因为那些头文件在编译方面是出了名的繁重。
不利用诸如 std::byte 等更平安的 C 类型替代类型,从而降低了类型平安性和可表达性。
每次经历丰硕的 C++ 法式员向游戏开发者定见利用更平安、更难以被误用的笼统时,他们都不会听——他们承担不起如许做的代价。因而,在其他范畴工做的人会认为游戏开发者是尚未发现笼统概念的原始人,喜好用指针和宏来玩火,完全意识不到招致他们利用那些手艺的原因。
另一方面,游戏开发者会讪笑和避开那些信奉高级笼统和类型平安的 C++ 法式员,因为他们没有意识到调试性能和编译速度可能没有更清洁、更平安、更心爱惜的代码那么重要。
我也没有任何证据证明那一点,但我思疑,怀着优化调试体验的愿望编写初级代码最末会增加调试的频次。
若是有人想要制止利用能够让他们的代码变得更平安的笼统,他们将不成制止地写出更多的 Bug,从而需要停止更频繁的调试。一旦 Bug 被修复,他们就会对调试器赞扬有加,并更有动力通过编写初级代码来连结高调试性能。那是一个恶性轮回!
在调试形式下启用优化
我晓得你在想什么——你认为那些游戏开发者无能,因为他们可能不断在利用 -Og!
你错了。
起首,-Og 只在 GCC 上可用。Clang 承受了那个标记,但它与 -O1 完全不异——LLVM 敬服者从未实现过安妥的调试优化级别。MSVC 没有与 -Og 相对应的工具,而大大都游戏开发者利用 MSVC 做为他们的次要编译器!
即便 -Og 无处不在,但它仍然不及 -O0——关于高效的调试会话来说,它可能仍然内联了太多代码。
任何高于 -Og 的优化级别都将招致十分蹩脚的调试体验,因为编译器将施行激进的优化。
我们能够做些什么
有几个方面能够改良——语言自己、编译器、原则库。
关于问答
问:人们应该写出包罗更少 Bug 的代码,如许他们就不需要调试了!
答:或许……但是,调试器不只用于找出 Bug 发作的原因,它还有其他用处。例如,有些人用调试器领会不熟悉的代码,或者找出无法找到的逻辑错误。
问:受那个问题影响的人不克不及有选择地只为某些文件停止无优化编译吗?
那在手艺上是可能的,但在理论中很难实现。起首,若是你正在调试,你其实不总能晓得需要查抄哪些处所——你可能会做出一个有根据的揣测,只禁用一些相关模块中的优化,但你可能是错误的,并且如许会浪费你的时间。
此外,许多构建系统可能不容易撑持那种基于单个文件的优化标记。我能够想象,在较老的代码库或专有 / 遗留构建系统中实现那个设法可能会十分困难。
最初,不要忘了,间接处理那个问题,而不是绕过它,我们还能够从中获得其他益处,好比更快的编译。
原文链接:
让小型企业进步 20 倍效率的同一手艺栈
60 岁周星驰雇用 Web3.0 人才,要求“宅心仁厚”;马斯克方案裁掉推特 75% 的员工;Linus 致开发者:不要再熬夜了 | Q 资讯
可能是最严峻的云存储数据外泄变乱之一:微软认可办事器错误设置装备摆设招致全球客户数据泄露
上云“被坑”十年末放弃,寒冬里第一轮“下云潮”要来了?
活动保举
致列位活泼在写做社区的开发者们!
你们是用代码书写将来的造梦者,也是用热情铸就将来开辟者。一年一度的开发者节如约而至!你们筹办好和我们一路“程”风破浪,披荆棘了吗?