【AICon】探索RAG 技术在实际应用中遇到的挑战及应对策略!AICon精华内容已上线73%>>> 了解详情
写点什么

令人沮丧的 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:244011

评论 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 · 新加坡
回复
没有更多了
发现更多内容

技术实践丨基于MindSpore框架Yolov3-darknet模型的篮球动作检测体验

华为云开发者联盟

AI 华为云 modelarts

如何保障企业数据资产的全生命周期安全?看这篇就够了

华为云开发者联盟

数据 数据资产 数据安全

分享一份大佬的MySQL数据库设计规范,值得收藏

小Q

学习 架构 面试 JVM 多线程

【领福利啦】广受欢迎的人工智能实战课程+“智能音箱”教程免费领!

小冬

人工智能 技术 福利 项目实战 智能音箱

《迅雷链精品课》第八课:迅雷链多链结构

迅雷链

区块链

看了 5 种分布式事务方案,我司最终选择了 Seata,真香!

程序员小富

Java 分布式事务 seata

广电总局严打劣迹主播:净化行业环境迫在眉睫

石头IT视角

甲方日常 58

句子

工作 随笔杂谈 日常

继linux命令之后,我又给你们整理了网络命令归纳,快给我来收藏

北游学Java

Linux 网络协议 网络 网络层

高德最佳实践:Serverless 规模化落地有哪些价值?

阿里巴巴云原生

阿里云 Serverless 云原生

11.11数据可视化大屏设计揭秘

京东科技开发者

大数据 AI 数据分析 数据可视化 交互设计

最详细的Linux TCP/IP 协议栈源码分析

linux大本营

Linux 后台开发 网络编程 C/C++ TCP/IP

谈谈持续集成、持续交付和持续部署三者究竟是什么,有何联系和区别呢!

ShenDu_Linux

Linux 持续集成 架构师 持续交付 持续部署

一文带你了解两种Transformer文字识别方法

华为云开发者联盟

人工智能 AI 文字识别

通过python基于netconf协议获取网络中网元的配置数据,助力企业网络控制自动化轻松实现!

华为云开发者联盟

通信 企业 网络自动化

GitHub 标星 1.3k+,一款超赞的用于字符串处理的 Java 8 库,附带源码分析

沉默王二

Java GitHub 字符串

红外遥控接收发射原理及ESP8266实现

IoT云工坊

人工智能 物联网 esp8266 红外遥控 pwm

监控之美——Prometheus云原生监控

华章IT

运维 云原生 监控 Prometheus

CPU虚拟化系列文章1——x86架构CPU虚拟化

华章IT

云计算 Linux cpu 操作系统 虚拟化

收藏 | 阿里程序员常用的 15 款开发者工具(2020 版)

阿里巴巴云原生

阿里云 程序员 开发者 云原生 Java 25 周年

Week1 命题作业

J

极客大学架构师训练营

Linux 服务器开发学习路线总结(配图 c/c++ )后台开发、Golang后台开发、后端技术栈

Linux服务器开发

Linux 后台开发 后端 Linux服务器 Go 语言

感恩,改变世界的开发者们!

京东科技开发者

开发者 程序人生

LeetCode题解:17. 电话号码的字母组合,队列,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

接口测试和性能测试的区别

测试人生路

软件测试 性能测试 接口测试

基于 GraphQL 的信息聚合网关的实现与展望

QiLab

高并发系统设计 graphql

前嗅教你大数据:常见的网站反爬策略与解决方案

前嗅大数据

大数据 数据采集 代理IP 网站反爬 反爬策略

索引为什么能提供查询性能...

小林coding

MySQL 索引 数据结构与算法 B+树

支付宝阿牛整合Netty+Redis+ZooKeeper「终极版」高并发手册

Java架构追梦

Java redis zookeeper 面试 Netty

没想到我费劲心力学的kafka,还不如阿里大佬整理的这份学习手册,真的是差距啊

小Q

Java kafka 学习 架构 面试

秋招offer收割机——后台服务器开发方向(专业学习路线图总结)

程序员小灰

c++ Linux 后台开发 架构师 服务器端开发

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