【FCon】聚焦金融行业在数智化的全面革新,一线的金融数智化实践干货 了解详情
写点什么

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

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

    阅读完需:约 14 分钟

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

6 月 17 日,极客时间《企业级 Agents 开发实战营》正式上线,10 周掌握企业级 Agents 从设计、开发到部署全流程。

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


大多数 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:244059

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

库调多了 都忘了最基础的概念-进程/线程篇

知识浅谈

9月月更 线程与进程

你真的理解C语言的灵魂 “ 指针 ” 吗?(初阶篇)

Albert Edison

指针 C语言 野指针 9月月更

数据治理(九):Atlas界面操作

Lansonli

数据治理 Atlas 9月月更

边缘服务网格 osm-edge 数据平面基准测试

Flomesh

Service Mesh 服务网格

《小米创业思考》之三:互联网七字诀

郭明

读书笔记

小程序能否成为电商的突破口

Geek_99967b

小程序 小程序开发

围绕“开源+深耕”策略和数字化监控手段,动态管理场景生态价值

易观分析

银行 易观 场景金融

Spring5源码14-SpringMVC-HandlerMapping

Java快了!

springmvc

SAP系统和微信集成的系列教程之二:如何通过微信公众号消费API

Jerry Wang

API 系统集成 SAP 微信开发 9月月更

京东前端面试题

loveX001

JavaScript 前端

数据中台改名DaaS平台?究竟什么是数据即服务(DaaS)?

雨果

DaaS数据即服务

数据库的视图怎么用?

阿柠xn

MySQL 运维 视图 数据库· 9月月更

【FAQ】接入华为应用内支付服务常见问题解答

HMS Core

SPL工业智能:发现时序数据的异常

石臻臻的杂货铺

SPL 9月月更

深入学习SAP UI5框架代码系列之三:UI5 控件的渲染器

Jerry Wang

JavaScript 前端框架 SAP UI5 ui5 9月月更

[极致用户体验] 在微信大字号模式下,网页样式乱了怎么办?

HullQin

CSS JavaScript html 前端 9月月更

NFT商城开发——NFT数字收藏平台开发解决方案

开源直播系统源码

NFT 元宇宙 数字藏品 数字藏品开发

Linux系统安装MySQL

MySQL Centos 7 navicat 9月月更

美团前端一面常见面试题

beifeng1996

JavaScript 前端

时代变了,企业网站应该这么策划内容

石头IT视角

SAP系统和微信集成的系列教程之一:微信开发环境的搭建

Jerry Wang

系统集成 SAP 微信开发 微信平台 9月月更

消除 JavaScript 的一些“异味”

掘金安东尼

JavaScript 前端 9月月更

计算机网络——速率相关的性能指标

StackOverflow

计算机网络 编程‘ 9月月更

剖析智能运维的五大应用场景

穿过生命散发芬芳

智能运维 9月月更

自适应熔断原理分析与源码解读

万俊峰Kevin

Go golang 熔断 go-zero 限流熔断

如何重新评估未完成的工作

ShineScrum捷行

Scrum 敏捷 DoD 未完成的工作

2022-09-06:以下go语言代码输出什么?A:Hi All;B:Hi go All;C:Hi;D:go All。 package main import “fmt“ func app() f

福大大架构师每日一题

golang 福大大 选择题

分布式中灰度方案实践

Java 架构

看得懂又好看的数学书,万人亲测的硬核教程!

博文视点Broadview

深入学习SAP UI5框架代码系列之四:HTML原生事件 VS UI5 Semantic事件

Jerry Wang

JavaScript SAP SAP UI5 ui5 9月月更

Java进阶(八)Java加密技术之对称加密、非对称加密、不可逆加密算法

No Silver Bullet

对称加密 非对称加密 9月月更 不可逆加密

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