【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

令人沮丧的 C++ 调试性能

  • 2022-10-25
    北京
  • 本文字数:4413 字

    阅读完需:约 14 分钟

令人沮丧的C++调试性能

到目前为止,“‘零成本抽象’是一个谎言”应该(希望如此)已经成为一个常识了。公平地说,这更像是用词不当——“抽象在经过优化后可能提供零运行时开销”这样的说法可能会更恰当一些,但我知道为什么不是这么回事……


大多数 C++ 程序员倾向于接受这样一个事实——“零成本抽象”只在启用了优化的情况下才能提供零运行时开销,而且它们对编译速度有负面的影响。同样是这些人,他们倾向于相信这种抽象是如此的有价值,以至于认为让他们的程序在调试模式下执行得很差(即没有启用优化)和编译得更慢是值得的。


我曾经也是他们中的一员。


然而,在过去的几年里,我开始意识到,在某些领域拥有高性能调试和快速编译是多么的重要,比如游戏开发。从事游戏开发的人往往直言不讳地说 C++ 的抽象与他们的工作格格不入,而且他们有充分的理由——游戏是实时模拟的,即使在调试版本中也需要可玩性和响应性——想象一下在 20FPS 左右的帧率下调试虚拟现实游戏导致眩晕的情形。


在本文中,我们将探讨 C++ 的抽象模型如何严重依赖编译器优化,并揭示一些导致意外性能损失的例子。之后,我们将比较三种主要编译器(GCC、Clang 和 MSVC)在这方面的表现,并讨论一些潜在的改进或解决方案。


移动 int 很慢


我在今年的 ACCU 2022 大会上做了一场闪电演讲(“移动 int 很慢:调试性能很重要!”),演讲的题目具有挑衅意味——移动 int 怎么会很慢?


我们来看一下这段代码。


#include <utility>
int main(){ return std::move(0);}
复制代码


C++ 程序员应该知道 std::move(0) 在语义上与 static_cast<int&&>(0) 相同,而且大多数人都希望编译器不会为 move 生成代码,即使禁用了优化。结果是 GCC 12.2、Clang 14.0 和 MSVC v19.x 最终都会生成一个 call 指令。


你可能认为这没什么大不了的——毕竟,这里或那里多出一个额外的 call 指令又有什么关系呢?下面是一个高性能算法的例子,它的内部循环中包含了一个 move。


template <class InputIterator, class _Tp>inline constexprT accumulate(InputIterator first, InputIterator last, T init){    for (; first != last; ++first)#if _LIBCPP_STD_VER > 17        init = std::move(init) + *first;#else        init = init + *first;#endif    return init;}
复制代码


请注意 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(https://t.co/esFxAngT2D)。


从链接的 Compiler Explorer 示例(https://godbolt.org/z/8sdvra6xb)中可以看到,为 std::byte 的位移操作符生成的汇编非常糟糕,导致了对 CPU 可执行的最简单、最快的操作的 call 指令。当然,使用 char 并不会生成如此糟糕的汇编,即使完全禁用了优化。


后果是什么


这些低效率的结果对于 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 的优化级别都将导致非常糟糕的调试体验,因为编译器将执行激进的优化。


我们可以做些什么


有几个方面可以改进——语言本身、编译器、标准库。


我们可以说函数模板不是为强制转换和位操作创建轻量级抽象的正确模型,类模板和轻量级类型,如 std::vector::iterator,也是如此。

过去曾有人尝试为“卫生宏(Hygenic Macro)”引入一种语言特性来解决本文所描述的问题,特别是 Jason Rice 的 P1221(“参数表达式”)提议。可惜的是,这篇论文几年来都没有更新。

即使我们设法在语言中引入了“卫生宏”,也无助于现有的实用函数,这些实用函数在过去已经被标准化为函数和类模板——也就是说,它不会让 std::move 变得更好。也许我们可以发明一些类似 [[no_unique_address]] 结合 [[gnu::always_inline]] 的属性或向后兼容的关键字来强制编译器始终内联有标记的函数,不需要为它们生成代码。

我目前还没有具体的想法,不过这可能是一个值得探索的方向。


编译器可以在处理这些函数的方式上变得更聪明一些,它们确实正在朝着这个方向发展!


GCC 12.x 引入了一个新的 -ffold-simple-inlines 标志(这是因为我提交的 Bug 报告,https://gcc.gnu.org/bugzilla/show_bug.cgi?id=104719),它允许 C++ 前端折叠对 std::move、std::forward、std::addressof 和 std::as_const 的调用。文档提到它应该是默认启用的,但如果我不手动指定标志,就无法让编译器执行折叠——请参考 Compiler Explorer 上的示例(https://gcc.godbolt.org/z/KPGe3YYsG)。

Clang 15.x 也受到了我提交的 #53689 问题(https://github.com/llvm/llvm-project/issues/53689)的启发,也为相同的函数引入了类似的折叠调用(加上 std::move_if_noexcept,如果 GCC 维护人员忘记了的话)。这个似乎是默认启用的——请参考 Compiler Explorer 上关于 Clang 14.x 和 Clang 15.x 之间的比较(https://gcc.godbolt.org/z/7MjM53h7G)。

MSVC 还没有在这方面提供任何改进。

我必须说,看到 GCC 和 Clang 维护人员逐步改进调试性能,我感到非常高兴,也非常感谢他们。

无论如何,我不认为硬编码的函数是正确的解决方案。我支持编译器用一些非常规手段,但规则应该更通用一些。

例如,它们可以对由单个 return 语句(只包含一个强制转换)组成的函数执行折叠,然后也可以将规则放宽到任意包含单个“基本”操作的函数,也包括 std::byte 和 std::vector::iterator。如果能看到这样的东西,那就非常酷了!


最后,标准库实现本身也可以变得更加聪明和对用户友好。

例如,它们可以在 std::accumulate 中使用 static_cast<T&&>(x) 而不是 std::move(x)。此外,它们可以将简单的包装器函数标记为 [[gnu::always_inline]] 或一个等效的内置属性,强制编译器内联它们。

不幸的是,libc++ 的维护者并不喜欢这些想法。我认为他们的理由没有说服力,而且我在 GitHub 上非常明确地表达了我的观点,但他们没有让步。

我希望在这方面看到一些进展——也许用强制转换替换一些 std::move 和 std::forward 调用,并在合适的位置添加一些属性,让整个 C++ 社区受益。在一个已经完全不可读的代码库中加入非常小的可读性,这真的是不值得做这些变更的理由吗?我认为不是。


关于问答


问:人们应该写出包含更少 Bug 的代码,这样他们就不需要调试了!


答:或许……但是,调试器不仅用于找出 Bug 发生的原因,它还有其他用途。例如,有些人用调试器了解不熟悉的代码,或者找出无法找到的逻辑错误。


问:受这个问题影响的人不能有选择地只为某些文件进行无优化编译吗?


答:这在技术上是可能的,但在实践中很难实现。首先,如果你正在调试,你并不总能知道需要检查哪些地方——你可能会做出一个有根据的猜测,只禁用一些相关模块中的优化,但你可能是错误的,而且这样会浪费你的时间。


此外,许多构建系统可能不容易支持这种基于单个文件的优化标志。我可以想象,在较老的代码库或专有 / 遗留构建系统中实现这个想法可能会非常困难。


最后,不要忘了,直接解决这个问题,而不是绕过它,我们还可以从中获得其他好处,比如更快的编译。


原文链接:


https://vittorioromeo.info/index/blog/debug_performance_cpp.html


声明:本文为 InfoQ 翻译,未经许可禁止转载。


今日好文推荐


让小型企业提高 20 倍效率的统一技术栈


60 岁周星驰招聘 Web3.0 人才,要求“宅心仁厚”;马斯克计划裁掉推特 75% 的员工;Linus 致开发者:不要再熬夜了 | Q 资讯


可能是最严重的云存储数据外泄事故之一:微软承认服务器错误配置导致全球客户数据泄露


上云“被坑”十年终放弃,寒冬里第一轮“下云潮”要来了?


活动推荐


致各位活跃在写作社区的开发者们!


你们是用代码书写未来的造梦者,也是用热情铸就未来开拓者。一年一度的开发者节如约而至!你们准备好和我们一起“程”风破浪,披荆斩棘了吗?



2022-10-25 18:244025

评论 2 条评论

发布
用户头像
一、标题中 debug performance 翻译不恰当
译文:令人沮丧的 C++ 性能调试

原文:the sad state of debug performance in c++

debug performance 是指没有打开优化时的 debug 模式下性能(对比启用了优化的 release 模式)。
纵观全文,作者要表达的意思在第二段最后一句有明确指出,“他们的程序在调试模式下执行得很差”。
对标题的理解是 “ C++: 令人沮丧的 Debug 模式性能”。



二、原文的一些外链接缺失, 而它们能补全代码上下文的逻辑信息
在原文第一小节 moving an int is slow

证明 call 引用的汇编代码: see for yourself on Compiler Explorer.

libcxx 里常用的 std::accumulate 库函数中 std::move 的引用: (from libcxx, cleaned up a bit):



三、翻译失误,意思完全两样。
译文:
为 std::byte 的位移操作符生成的汇编非常糟糕,导致了对 CPU 可执行的最简单、最快的操作的 call 指令。

(看着很别扭,没看懂去翻了原文)

原文:
the generated assembly for the bitwise shift operators on std::byte is dreadful, resulting in a call instruction for possibly the simplest and fastest operation that a CPU can perform.

真实的意思:
对 std::byte 的位移操作生成的汇编非常糟糕,位移可能是一个 CPU 能执行的最简单、最快速的操作了,这里却生成了一个 call 函数引用。



结尾
用同样标题结尾 —— “令人沮丧的 C++ 翻译”,翻译看得我云里雾里,翻了英语原文才知道是什么意思。从信息的准确度上说,与原文偏离有点远。个人观感翻译水平像博客机器翻译,缺失 C++ 的技术校对,不像商业出品的味道。
感谢分享了这篇文章,内容本身很有启发,打开了新的窗子。
展开
2022-10-31 15:56 · 中国香港
回复
嗯嗯,感谢指正~
2022-10-31 19:18 · 新加坡
回复
没有更多了
发现更多内容

基于Vue构建低代码平台的思考

互联网工科生

Vue 低代码 表单 JNPF

PoseiSwap 开启“Poseidon”池,治理体系或将全面开启

西柚子

软件测试/测试开发丨Python 内置库 json

测试人

Python json 软件测试

低代码平台,需求和技术发展的产物

这我可不懂

软件开发 低代码平台 开发方式

代币头像logo申请方法盘点:薄饼最难、Ave最简单、TP钱包居中

加密先生

logo图标

接口测试实战| GET/POST 请求区别详解

霍格沃兹测试开发学社

技术分享 | app自动化测试(Android)--高级定位技巧

霍格沃兹测试开发学社

技术分享 | 测试平台开发-前端开发之Vue.js 框架

霍格沃兹测试开发学社

关于文件传输软件和传输大文件你需要知道的一切

镭速

传输大文件

5分钟,带你了解低代码开发

高端章鱼哥

低代码 数字化转型 应用开发

测试必会 Docker 实战(一):掌握高频命令,夯实内功基础

霍格沃兹测试开发学社

干货|app自动化测试之Appium问题分析及定位

霍格沃兹测试开发学社

全网首档操作系统探访体验栏目“龙蜥+超级探访”震撼上线!看国产 OS 如何乘风破浪

OpenAnolis小助手

开源 操作系统 龙蜥 统信软件 超级探访

PoseiSwap 开启“Poseidon”池,治理体系或将全面开启

大瞿科技

ThreadLocal

红袖添香

Java ThreadLocal ThreadLocalMap Java 线程

技术分享 | Selenium多浏览器处理

霍格沃兹测试开发学社

干货|app自动化测试之Capability 使用进阶

霍格沃兹测试开发学社

悦数图数据库:发布 AI 大模型解决方案,开启「图+ 大模型」应用新范式

悦数图数据库

数据库 图数据库 分布式图数据库 NebulaGraph 悦数

Last Week in Milvus

Zilliz

非结构化数据 Milvus Zilliz 版本更新 AIGC

8月征文:今天你 ARTS 打卡了吗?【中奖名单见文末】

InfoQ写作社区官方

ARTS 打卡计划 征文活动 热门活动

不可错过!12个编写整洁Java代码的最佳实践方法

SoFlu软件机器人

瓴羊Quick BI在Gartner魔力象限中脱颖而出

夜雨微澜

融云:从「对话框」跳进魔法世界,AIGC 带给社交的新范式

融云 RongCloud

人工智能 AI 通信 社交 AIGC

瓴羊Quick BI:数据大屏可视化展示,助企业提升竞争优势

巷子

文件传输软件的市场现状和未来趋势

镭速

文件传输软件

恭喜!杭州悦数成为「大数据技术标准推进委员会」2023 年度合作伙伴

悦数图数据库

数据库 图数据库 NebulaGraph

自动化实践-全量Json对比在技改需求提效实践

得物技术

json 自动化 测试 企业号 8 月 PK 榜

企业文件传输软件安全性分析与对比

镭速

文件传输软件

【我和openGauss的故事】在vm中安装openEuler及使用yum安装openGauss

daydayup

Jupyter Notebook 遇上 NebulaGraph,可视化探索图数据库

NebulaGraph

AI Jupyter Notebook 图数据库

Uiautomator2.0

霍格沃兹测试开发学社

令人沮丧的C++调试性能_语言 & 开发_Vittorio Romeo_InfoQ精选文章