1 会议介绍
大会简介:系统级软件是数字世界的基础设施,C++ 自 1985 年由 Bjarne Stroustrup 博士在贝尔实验室发明以来,一直被誉为系统级编程“皇冠上的明珠”。 秉承“全球专家、卓越智慧”的理念,我们特邀全球 C++ 和系统级软件技术领域的大师、专家、学者,汇聚一堂。大会围绕现在 C++ 最佳实践、架构与设计、大模型驱动的软件开发、AI 算力与优化、系统级软件及其他编程语言,深度探讨系统级软件技术领域的最佳工程实践和前沿方法。我们致力于将此次大会打造为系统软件领域规模最大、阵容最强、干货最多的高规格技术盛会!
2 收获与总结
会议记录:2024-12-05-全球C++及系统软件技术大会
总体而言,参加该会议是有收获的。在本次会议中,各位专家更多的是对 C++23 新特性的介绍应用,C++ 之父和部分 C++ 委员会对 C++26 中的一些提案进行了分析和展望。
虽然我们一致对很多 C++ 新特性表示欢迎,因为这些新特性能大大减少我们的开发工作量,但奈何现实情景是,目前公司内部 C++11 铺开没多久。尽管我们团队已经是目前走的比较快的一批了,但也仅停留在 C++17,还未有机会接触更新特性。
除了那些“遥远”的新特性,我在会上接触了不少我熟悉的知识,这让我感觉到一些开心,至少就开发规范方面,我们并不”落后“。当然,我也接触了不少不熟悉的知识,这让我注意到原来还有一些之前忽略的、未曾探索过的领域,或许未来会在这些领域尝试探索。
接下来,我将从编码规范、开发工具以及一些优化手段归纳总结本次会议中的收获。
2.1 编码规范
2.1.1 资源管理与生存期管理
在会上 Bjarne Stroustrup 着重强调了程序资源管理的重要性,提出了一些“现代 C++”开发者应该注重的生存期管理规范:
资源管理
- 资源是指任何必须获取并在之后释放 (归还) 的对象
- 可以显式或隐式释放
- 包括: 内存、字符串、互斥锁、文件句柄、套接字、线程句柄、着色器…等
- 杜绝资源泄漏
- 避免手动释放资源
- 应用代码中不应使用 free()、delete 等资源释放操作
- 每个对象都有对应的句柄
- 负责访问和释放资源
- 应用代码中不应使用 malloc()、new 等返回裸指针的资源获取操作
- 所有资源句柄都属于特定作用域
- 句柄可以在作用域间转移
生存期管理
高效的资源管理必需的几个要素
- 构造
- 首次使用前建立对象的不变量 (如果有的话)。
- 通过构造函数实现
- 析构
- 最后使用后释放所有资源 (如果有的话)
- 通过析构函数实现。
- 拷贝
- 语义:
a=b implies a==b(常规类型)
- 拷贝构造函数:
X(constX&)
- 拷贝赋值:
X::operator=(const X&)
- 移动
- 在作用域间转移资源所有权
- 移动构造函数:
X(X&&)
- 移动赋值:
X::operator=(X&&)
2.1.1.1 RAII(资源获取即初始化)
RAII 是一个目前普遍使用的资源管理的方法。在我入职时,带我的导师就强调了这种编码方式。或许称为“风格”,又或者是一种“规范”。
2.1.1.2 智能指针与引用
Bjarne 提出的观点我是这么理解的:堆资源使用句柄的方式管理。
在我们近期开发的项目中,我们经常会为了避免内存拷贝采取一系列的“手段”:
比如在多读场景下,资源以 shared_ptr
的形式在特定容器内存在,我们同样以 shared_ptr
的方式获取资源。当资源需要删除时,我们只需要对容器进行保护并删除容器内元素即可,无需再关注资源是否正在被使用。如此可以保证资源在读取时不会产生内存错误,但在一些场景下可能会导致资源无法释放的问题。
2.1.2 错误处理
Bjarne Stroustrup 认为错误处理需要符合以下规则
- 需要有明确的错误处理策略
- 错误码和检查
- 用于处理常见且可在局部处理的失败情况
- 避免使用效率低下且丑陋的 try .. catch
- 我们经常忘记检查错误码,导致错误结果
- 不适用于构造函数和运算符,例如 Matrix x= y+z;
- 异常机制
- 用于处理罕见且无法在局部处理的错误
- 错误可沿调用链向上传播
- 避免陷入“错误码地狱”
- 未捕获的异常会导致程序终上而不是产生错误结果
- 不能与原始指针资源句柄配合使用 - 应当使用 RAII 机制的作用域资源句柄
这些规则很简单,也很精炼。回想起我已经做过的项目,发现我对异常的处理方式逐渐趋近于 Bjarne 提出的规则。
2.1.2.1 需要有明确的错误处理策略
由于我们目前开发的系统大多是内部系统,无需对外提供,同时我们自身也会参与系统的运维——当生产上出现异常,开发人员也需要及时到场解决问题——因此我们对异常处理的看法更多是帮助定位问题,为人工解决提供支持。
这就意味着我们的程序运行需要依赖一系列的“应急手册”用于处理一些偶发的小概率事件。这对运维人员的工作带来了挑战,但由于系统性质的原因,在一些场景下,程序宕掉远比给出错误结果要好很多。
这种方式的好处是显然易见的。我们无需在程序内部尝试覆盖所有的异常场景,无需对每种异常场景以编码的形式“写对策”,开发成本自然下降了很多。虽然苦了运维人员,但能避免错误数据对核心流程带来未知影响。
2.1.2.2 错误码
错误码对我们项目团队而言是一个“伪命题”。
在此前的一个“行情平台”项目中,我们尝试仿照 Oracle 的 Instantclient 引入错误码,尝试方便运维人员定位和解决问题。
开始我们都认为这或许是一个不错的方式,直到一两个版本后,我们发现错误码需要一套体系来保证可用,这或许需要经验,或许需要一个验证过的方法论。
总之,我们发现查错误码时十分痛苦,在敏捷迭代时偶尔会出现错误码冲突的情况,如果专设一个人员负责统筹管理错误码,那么该人员将会成为项目开发的一个短板,拖累整个项目进度。
或许当项目足够庞大且需要对外部暴露交付的时候才需要考虑错误码?我不知道。但我知道陷入错误码地狱中的痛苦。
错误码地狱 “错误码地狱”指在软件开发中,大量分散且难以管理的错误码充斥代码,导致程序可读性、可维护性变差。开发者要花费很多时间处理错误码,还可能因混淆错误码的含义而引入新的错误,就像陷入“地狱”般痛苦。
2.1.2.3 错误可沿调用链向上传播
我们都看过 Java、Python 的错误和异常信息,它们能显示异常发生的位置和对应的调用栈。有一说一,我也希望 C++ 能支持这一点,这会大大减少定位错误的难度。
当然,引入这个特性的代价我也是不想付出的,这必然会带来运行开销,就当我在想 peach 吧。
目前我们采用的方式更多的是通过异常日志的方式将调用栈展现出来,也算是一种另类的向上传播,但当我们尝试开发一些通用的组件时,如何更好的展示调用链又成了一个问题。
在近期的项目中,我也尝试过解决这个问题,目前我认为比较好的方式是使用 JResult 和 BaseLog 双管齐下。前者是仿照 Rust 的 Result 方法实现的一个包裹返回值和异常的类,后者是用于和日志库解耦的中间日志组件,目前在外网放的都只是原型代码。
通过这种方式,我们也做到了错误的向上传播,也算“紧跟潮流”。
2.2 开发工具
大会就像是个推介会,各家产品层出不穷,有大模型帮忙写代码找改 bug 的文心快码,也有帮助 XC 性能优化的检查工具等等。
这里列几个感兴趣的,但都还没有尝试,未来有时间探索。
2.2.1 pre-commit:内嵌到 git 中的检查工具
官方简介:Git 钩子脚本对于在提交到代码审查之前识别简单问题很有用。我们在每次提交时运行钩子,以自动指出代码中的问题,例如缺少分号、尾随空格和调试语句。通过在代码审查之前指出这些问题,这允许代码审阅者专注于更改的架构,同时不会用琐碎的样式挑剔浪费时间。随着我们创建了更多的库和项目,我们认识到跨项目共享我们的提交前钩子是痛苦的。我们从一个项目复制并粘贴笨重的 bash 脚本,并且不得不手动更改钩子以适用于不同的项目结构。我们相信您应该始终使用最好的行业标准 linter。一些最好的 linter 是用您在项目中不使用或已安装在您的机器上的语言编写的。例如,scss-lint 是用 Ruby 编写的 SCSS 的 linter。如果您在 node 中编写项目,您应该能够使用 scss-lint 作为预提交钩子,而无需向您的项目添加 gemfile 或了解如何安装 scss-lint。我们构建了预提交来解决我们的钩子问题。它是预提交钩子的多语言包管理器。您指定所需的钩子列表,预提交在每次提交之前管理以任何语言编写的任何钩子的安装和执行。预提交专门设计为不需要 root 访问权限。如果您的开发人员之一没有安装 node,但修改了 JavaScript 文件,预提交会自动处理下载和构建节点以在没有 root 的情况下运行 eslint。
2.2.2 reviewdog:自动化代码审查工具
官方网站:https://github.com/reviewdog/reviewdog
官方简介:reviewdog 提供了一种通过与任何 linter 工具轻松集成来自动向代码托管服务(如 GitHub)发布评论的方法。它使用 lint 工具的输出,并在发现要查看的补丁差异时将它们作为评论发布。reviewdog 还支持在本地环境中运行以按差异过滤 lint 工具的输出。
不过这个工具主要是用于 Github,理论上只要 API 兼容应该是可以移植的,但移植的难度感觉还是略大。
2.3 优化手段
2.3.1 分支预测
李成栋提到分支预测可能反而会拖累运行。
在一些特定条件下,可以使用 __restrict__
关键词优化。
2.3.2 常量传播
2.3.3 通用循环优化
2.3.4 向量优化
2.3.5 XC 优化
李浩在国产处理器优化方面有研究,提出:程序级迁移至 XC,即便同属 X86 平台,也会有性能损失。源码级迁移至 XC,也同样有一定的性能损失,这源于 gcc 未使用更优的指令集优化。
他们团队的做法是:
- 开启编译优化选项,这个和目标处理器的指令集有关,提升最大。
- 替换 GCC buildin 函数,进行专有指令的优化,关键路径提升很大。
- 对部分内存进行对齐(但不可盲目对齐,可能会因为指令集差异导致异常),特别是
#pragma pack
可能会导致奇怪异常,源于指令集取数据的长度可能不一致。
虽然该演讲旨在推广它们的迁移平台,只提问题不提解决方案,但不妨是一个优化思路。