看英伟达、Meta、阿里等巨头齐聚,破解40岁C++在新时代下的六大技术难题!

作者 | 《新程序员》编辑部
出品 | CSDN(ID:CSDNnews)

C++,这门由 Bjarne Stroustrup 在贝尔实验室孕育而生的编程语言,自 1979 年诞生以来,便以其高效与强大抽象能力并重的特点,成为了系统软件开发的基石语言。从那时起,每一次 C++ 的迭代与升级,都承载着解决当前时代问题的使命。

如今,站在新的历史节点上,C++ 之父在「2024 全球 C++ 及系统软件技术大会」主会上再度强调:C++ 将继续演进,以解决新时代所面临的全新挑战。

紧随 C++ 之父的步伐,本次大会分论坛汇聚了国内外 C++ 及系统软件领域的顶级专家,共同探索 C++ 在不同技术方向上所面临的复杂问题与解决之道:

  • 放眼全球,以 Adobe、英伟达和 Meta 为代表的一线技术专家带来了关于 C++ 程序库优化、并行编程、指针管理等关键议题的深刻解析,深入探讨如何在 C++ 中实现高效的设计与优化

  • 聚焦国内,阿里云开发工程师许传奇、系统内核专家张银奎与 Boolan 首席咨询师吴咏炜等业界翘楚,也围绕编译技术、应用移植以及函数式编程等话题展开讨论,着眼于 C++ 在现代软件开发中的多样化应用和技术革新。

那么接下来,让我们一同聆听他们对于 C++ 不同方向问题的独到见解,共同探讨 C++ 在新时代背景下的崭新机遇与挑战。

图片

Adobe 首席科学家、C++ 标准委员会委员 David Sankel——《开发高质量 C++ 程序库:来自 Beman 项目的见解》

作为 Adobe Software Technology Lab 的首席科学家,David Sankel 的团队被任命负责探索未来十年软件开发实践。在这场演讲中,他以新成立六个月的 Beman 项目为例,分享了开发高质量 C++ 库的最佳实践经验。

“支持高效设计和采用最高质量的 C++ 标准库”——这是 Beman 项目的核心使命。为实现这一目标,项目通过实际的实现经验、用户反馈收集和技术专业知识三个方面开展工作。目前,项目已经推出了多个可用的库,包括实现 C++26 std::optional 扩展的 Beman Optional 26,用于 UTF 转码的 Beman UTF View,以及简化标准迭代器创建的 Beman Iterator Interface。同时,In-place Vector、Beman Execution 26 和 Beman Net 29 等库也正在开发中。

图片

“当你面对一个空白目录时,要如何开始创建一个新的 C++ 项目?” Sankel 道出了许多开发者的困扰。互联网上充斥着各种互相矛盾的建议,有的带有平台特定的偏见,有的源于不同的开发文化。为应对这些挑战,Beman 项目提出了两个核心原则。

第一个原则是采用行业标准技术。“在有行业共识的最佳实践上,我们应该加以利用,” Sankel 说,“创新工具和风格并不是我们的目的。” 项目团队通过数据驱动决策,例如从 isocpp.org 年度调查中发现 CMake 占据 83% 的市场份额后,将其确定为标准构建系统。这一选择也从 Boost 项目汲取了教训——自研工具往往难以推广,最终徒增维护负担。

第二个原则是建立欢迎和包容的社区。为了获得广泛而有用的反馈,项目致力于为使用、审查和贡献创造畅通的途径。有趣的是,对于代码风格这类品味问题,项目采用了一个独特的方案:指定一位同事负责所有风格决定,避免在非关键问题上消耗时间。

图片

这些原则在 beman.exemplar 项目模板中得到了具体体现。项目采用清晰的目录结构,使用标准的工具链进行开发,并建立了完善的开发环境配置。特别值得一提的是自动化工具的应用,如使用 pre-commit 工具实现提交前检查,以及引入 ReviewDog 实现自动代码审查,这些都极大地提升了开发效率。

“Beman 项目代表了我们对于新 C++ 项目最佳实践的理解,” Sankel 在总结时说,“但这些实践会随着业界的发展而不断进化。”通过 discourse.bemanproject.org 和 GitHub,项目持续收集社区反馈,不断完善这套实践标准。这种开放和进取的态度,正是确保 C++ 程序库持续保持高质量的关键。


图片

英伟达首席架构师 Bryce Adelstein Lelbach——《用“并行”的方式思考》

“通常情况下,我们人类是按顺序思考的。并行计算常被视为具有挑战性且复杂的技术。我们把它当作一种需要谨慎使用的工具,仿佛只有专家甚至编程高手才能驾驭。但今天我们生活在一个并行的世界,从最小的嵌入式设备到最大的超级计算机,几乎每个硬件平台都是并行的。因此我们必须改变思维方式,每个编写代码的人都必须学会并行思考。并行必须成为我们的默认选项。”英伟达(NVIDIA)首席架构师 Bryce Lelbach 以这段来自内心的感悟开启了他的演讲。

图片

Lelbach 选择了一个基础的并行原语 —— scan 算法来阐述并行思维。在 C++ 标准库中,scan 算法有 inclusive scan 和 exclusive scan 两种形式。本次演讲聚焦于 inclusive scan,它接收一个范围和一个二元运算符,为每个位置生成从开始到该位置的所有元素的和。由于 scan 保持输入顺序,操作不需要满足交换律,但要实现并行,操作必须满足结合律(associative)并具有左幺元(left identity value)。

通过一个字符串连接的例子,Lelbach 展示了序列“a b c d e f g h i”的 scan 过程:每个输出元素都是从第一个元素到当前位置的所有元素的累积结果。Lelbach 特别指出:“注意最后一个元素是整个序列的和,这是一个我们后面会用到的重要特性。”

图片

在讲解并行化方法时,Lelbach 提出了处理并行问题的三个关键要素:“当我处理并行问题时,我会将其分解为三个部分:如何划分和分配工作?在工作的每个分块上需要执行什么计算?以及在这些分块之间需要进行什么通信?其中,通信是最重要的部分,因为它往往决定了如何分配工作以及何时可以计算。”

为了实现高效的并行,Lelbach 提出了两个指导原则。第一是本地化同步(localize synchronization):必须同步时,要最小化同步范围,只与必需的参与者同步。第二是隐藏延迟(hide latency):如果必须等待,需要找到其他有用的工作来填充等待时间。

基于这些原则,Lelbach 介绍了 decoupled lookback 策略,这是 GPU 上 scan 算法的基础实现。这种策略的核心是 scanTileState 对象,它通过描述符(descriptor)存储两种前缀:local prefix(仅包含该 tile 的元素和)和 complete prefix(包含从开始到该 tile 的所有元素和)。描述符使用原子状态变量指示这些前缀的可用性。

图片

在实验中,使用 decoupled lookback 的实现在处理 4GB 数据时,相比需要两次全局同步的传统方法快了约三倍。正如 Lelbach 所说:"在 GPU 和分布式系统规模上,性能差距会更大。"

Lelbach 通过 copy_if 和 chunk_by 两个实例展示了 scan 的实际应用。使用 decoupled lookback 的 copy_if 在 64 核 CPU 系统上处理 4GB 问题时比三段式实现快 11 倍,而 chunk_by 在处理 1GB 字符输入时则快 9 倍。

Lelbach 总结道,“scan 可以用来解决任何具有求和、计数、过滤、搜索或邻居感知特性的问题。就像我在职业生涯初期从 Hartmut Kaiser 那里学到的:用并行的方式思考可以让我们更好地应对现代计算的挑战。”


图片

Meta 资深内核专家,世界级并行编程专家 Paul McKenney——《生存期结束时的指针清零》

指针在生命周期结束时的处理一直是 C++ 中的棘手问题。2017 年在多伦多的 C++ 标准委员会会议上,Hans Boehm 向 Meta 资深内核专家 Paul McKenney 和 Meta 技术专家 Maged Michael 指出了风险指针(hazard pointers)实现中的一个问题,这引发了人们对 C++ 中指针生命周期管理的深入思考。

图片

问题最初出现在风险指针的 try_protect() 函数中。虽然这个特定问题可以通过修改代码来解决,但它揭示了一个更深层的问题 —— 高级语言中的指针处理与底层实现之间的差异。为了深入说明这个问题,McKenney 以 LIFO(后进先出)栈为例,展示了所谓的 ABA 问题:当一个线程持有指向对象 A 的指针时,另一个线程可能已经释放了 A 并重用了该内存位置来存储新对象 C。

图片

“这在汇编语言中工作得很好,因为汇编语言只关心指针的位值,” McKenney 解释道,“但在 C++ 中,编译器会基于指针的溯源(provenance)信息进行优化。” 这个问题的严重性不容忽视,因为 LIFO 栈已有很长的历史。它在 1986 年就被 Treiber 在 IBM BAL 中描述过,现在仍在 Linux 内核的 llist_head 等实现中广泛使用。

在现代编译器中,指针不仅包含内存地址,还包含溯源信息。这些溯源信息记录了对象的分配时间和位置,编译器用它们来进行优化。例如,一个简单的指针比较代码:

q = malloc(sizeof(*q));do_something(q, q == p);

编译器可能优化成:

do_something(q, false);

一些硬件平台已经开始在硬件层面支持指针溯源。比如 ARM 的内存类型扩展(Memory Type Extensions, MTE),它在指针的高位bits中存储了 generation 信息。当内存被重新分配时,这个代际号会增加,从而能够检测到使用已失效指针的行为。

针对这个问题,业界探索了多种解决方案。一种方案是使用 RCU(Read-Copy Update)机制。通过延迟内存释放直到所有读取者完成操作,RCU 可以避免指针失效问题。但正如 McKenney 指出的:“这会增加 CPU 开销,并可能在小型嵌入式系统中积累大量等待释放的内存。”

最终的突破来自于 Davis Herring 在提案 P2434R2 中提出的“天使溯源”(angelic provenance)概念。这个方案允许编译器在指针被重新解释时(比如通过 integer 转换后)选择合适的溯源信息。如果有后续的比较或解引用操作,编译器必须选择一个能让程序行为良好定义的溯源信息。

“这是第一次核心语言开发者和并发编程专家达成共识,” McKenney 总结道。目前,通过提案 P2414R4(指针生命周期结束清除方案)和 P3347R0(无效和预期指针操作),这些改进正在 C++ 标准委员会中推进。尽管这些提案尚未最终确定,但它们代表了迄今为止解决这个问题的最佳进展。


图片

Boolan 首席咨询师,吴咏炜——《Lambda? Lambda!》

随着 C++ 标准自 C++11 以来的逐渐演化,lambda 表达式已然成了现代 C++ 惯用法里不可或缺的一部分,那么 lambda 表达式究竟要怎么用、又有何意义呢?为了解答这些问题, Boolan 首席咨询师吴咏炜带来了《Lambda? Lambda!》主题演讲。

“lambda 表达式是必不可少的语言特性吗?并不是。lambda 表达式是对 C++ 表达能力有革命性贡献的语言特性吗?绝对是。”

图片

lambda 演算的起源,最早可追溯到 1936 年数学家阿隆佐·邱奇提出的形式系统,在这个系统中,可以使用所谓的“λ-项”来表达像数字和函数这样的基础概念。而当我们将视线转向编程时,可以发现 lambda 演算的思想被逐步引入到了各种实际编程语言中。

图片

吴咏炜指出,虽然 lambda 表达式有替换的表达形式,但从缺少 lambda 表达式支持的 C++98 里我们可以看到,结果是人们使用各种奇技淫巧来提供相似的功能——而这些方式都存在各种各样的缺陷。C++11 终于引入了 lambda 表达式,实质上是编译器为我们直接提供特殊的函数对象,以方便代码的表达。很快,这一特性已经成为 C++ 编程中不可或缺的一部分。lambda 表达式让代码更加简洁、易读,并极大地提高了编程的灵活性和效率。在 C++17 中更是带来了更多改进,比如捕获列表的增强、泛型 lambda 表达式等。他也总结了几种 lambda 表达式的捕获方式:

图片

紧接着,吴咏炜通过一系列代码示例,展示了 lambda 表达式在 C++ 中的各种使用场景。

他强调了函数对象的性能优势,即使用函数对象(含 lambda 表达式)传参给函数模板容易进行内联。

图片

他也提供了标准算法与 lambda 表达式结合使用的示例,如 sort、copy_if、generate,等等。此外,吴咏炜还探讨了带状态变化的 lambda 表达式、C 风格回调与 lambda 回调的对比,以及立即求值的 lambda 表达式和 lambda 表达式作为代码组织方式的优势,例如:为代码块提供统一的出口类型,在比简单条件和循环更复杂的情况下 lambda 表达式具有更大的灵活性等。他展示了,在代码存在重复的情况下,lambda 表达式能大大简化代码。

图片

在现代 C++ 里,lambda 表达式使得一些通用代码成为可能。如果没有 lambda 表达式的话,那基本上就无法对这样的代码进行抽象了。他举的其他例子包括利用 lambda 表达式传递 constexpr 参数,及在自定义的反射系统里使用泛型 lambda 表达式封装遍历不同类型字段的代码。

图片

不过,吴咏炜也指出 lambda 表达式在 C++20 之前存在一些限制,为此他特别介绍了 C++20/23 对 lambda 表达式的改进,如在未求值上下文使用 lambda 表达式、捕获结构化绑定、lambda 自引用等。在演讲最后,吴咏炜强调 lambda 表达式是一种工具而非目的,它改变了我们组织代码的方式,并在每个 C++ 标准中变得越来越强大和易用。


图片

阿里云开发工程师,许传奇——《ClangIR:Clang 编译器的下一个最重要更新》

编译器作为连接源代码与可执行程序的桥梁,其重要性不言而喻。其中,Clang 作为 C++ “新” 生代编译器的代表产物已经诞生了 20 余年,而随着 C++ 语义的不断复杂化以及人们对于程序安全性的日渐关心,Clang 亟需重要更新。在此背景下,阿里云开发工程师许传奇为我们带来了一场关于 Clang 编译器的深度分享:《ClangIR:Clang 编译器的下一个最重要更新》。

图片

演讲伊始,许传奇简要介绍了 ClangIR 的定义与来源。IR(Intermediate Representation)即中间表示,是编译器内部用于程序流、数据流分析与优化的数据结构。而 ClangIR 则是基于 MLIR(Multi-Level Intermediate Representation)框架、针对 C++ 程序设计的中间表示,可以方便地创建新的 IR 并复用已有 IR 的能力。

接下来,许传奇通过一系列代码示例,系统地阐述了 ClangIR 的应用价值。他指出,IR 之所以受到青睐,是因为相较于 AST(抽象语法树)只能获取局部信息且难以变换的局限性,IR 在数据流与控制流分析方面更具优势。然而,传统的 LLVM IR 由于缺乏语言信息,限制了对 C++ 程序的深入优化。而 ClangIR 的出现正是为了弥补这一空白,它结合了 AST 和 LLVM IR 的优点,既保留了必要的语言信息,又便于进行复杂的分析和优化。

例如在协程优化方面,ClangIR 可通过证明协程的声明周期收敛于当前函数,将协程的内存分配从堆上动态分配转变为栈上分配,避免了昂贵的堆内存分配。这一过程类似于 NRVO,利用 ClangIR 的数据流分析能力,更准确地判断某个协程是否会被 co_await、其声明周期是否收敛。

在提升 C++ 程序安全性方面,ClangIR 也发挥着重要作用。许传奇介绍了一种名为 Borrow Checker 的内存安全检查工具,该工具最初源自 Rust 语言。而通过 ClangIR,可以将 Borrow Checker 以兼容语言扩展的形式加入到 C++ 中,增强安全性的同时不引入任何新语法与语义。

此外,许传奇还提到了 ClangIR 在 C++ 模式匹配与优化方面的应用:借助 ClangIR 携带的 C++ 信息,可以对常见的 C++ 模式进行识别并替换为更高效的代码。这一特性不仅提高了程序的运行效率,还降低了开发者的编程难度。更值得一提的是,在 C++ 语言信息之外,ClangIR 还可以利用 MLIR Dialects 方言所提供的能力来进一步提高程序的性能。

在演讲最后,许传奇总结了 ClangIR 的现状与未来展望:目前 ClanglR 社区活跃,项目处于孵化阶段并正逐步向 LLVM/Clang 主仓库提交,因此项目开发任务众多,对于开发者而言机会也很多。至于 ClangIR 的下一步优化方向,他透露道:目前优先级相对较高的问题为对 C++ 程序的表示问题。


图片

系统内核专家,张银奎——《从 Windows 到 Linux:C++ 应用移植实践》

在当今技术多元化的世界中,软件的跨平台兼容性变得尤为重要,尤其是对于需要在不同平台间移植的 C++ 应用而言。那么,把本来在 Windows 上开发的 C++ 程序移植到 Linux 要花多少时间,期间又会经历什么?为此,系统内核专家张银奎以一个真实的 C++ 项目为例,详细解答了这些问题:《从 Windows 到 Linux:C++ 应用移植实践》。

图片

在进入具体的移植实践之前,张银奎介绍了被移植项目的背景,NDB(Nano Code Debugger):一款基于 Windows 开发、功能强大的调试器,支持多种调试目标,包括 Linux 内核、Linux 本地应用程序、u-boot、UEFI 以及 Windows 应用。

“这样一个几十万行的 C++ 项目,要怎么把它从 Windows 搬到 Linux 上去?”张银奎指出,这个过程面临的主要挑战在于:要将 NDB 代码从 Windows 移植到 Linux,同时保持 Windows 版本的延续,尽量减少代码改动,并确保代码长远的可维护性。

基于此,他介绍了关键的移植策略。因为本来项目中的每个 .cpp 都会包含 StdAfx.h 头文件。利用这个特点,只要修改这个头文件,那么Windows 下的代码可以像以前一样工作,而在 Linux 下则包含一个模拟 Windows 头文件的 wintype.h。

接着,他介绍了在 API 层模拟 Windows 的三个锦囊,即宏定义、类型定义( typedef),以及模拟函数(难以使用宏的地方)。有了这三个锦囊后,使得在 Linux 环境下模拟 Windows 行为成为可能,包括对基本类型的处理、API 调用的映射以及异常处理的转换。

例如在基本类型方面,Windows 和 Linux 在大小写偏好上存在差异(Windows 喜欢大写,Linux 则偏好小写),张银奎建议在 wintype.h 中使用 typedef 定义统一的基本类型。

图片

对于 Win32 API 的移植,他也提出了三种方法:简单宏定义、复杂宏定义和等价实现。简单宏定义适用于有对应 API 且原型一致的情况;复杂宏定义适用于有类似实现但函数原型不一样的情形;等价实现则用于 Linux 下没有现成实现的情况,需要自定义函数来模拟 Windows API 的行为。

而在整个移植过程中最复杂的部分之一,就是结构化异常处理的问题。张银奎解释道,结构化异常处理是 Windows 平台的一个重要特性,但 Linux 内核本身并不支持异常处理。因此当涉及到从 Windows 向 Linux 移植代码时,可以将其转换为等效的 C++ 异常处理语法。

另外在 Windows 和 Linux 之间进行代码移植时,考虑到二者之间字符编码的不同也非常重要。张银奎提出了简化处理的策略,即在 Linux 下将字符串从 utf16(Windows 上的 wchar_t)转换为 ansi ,再使用 Linux 下的 API 将 ansi 转为 wchar_t,以解决字符编码的差异问题。

最后,张银奎提到关键区在 Windows 应用程序中很流行,并提出了在 Linux 下实现关键区重入的方法:使用 pthread_mutex_t 来模拟关键区,但由于 Linux 下的互斥锁默认不可重入会导致 BUG,因此可通过设置 PTHREAD_MUTEX_RECURSIVE 来解决这个问题。

图片

为了让更多技术爱好者紧跟行业最前沿,「2024 全球 C++ 及系统软件技术大会」特别推出「云会员」服务,让用户通过线上平台尽享全球顶尖技术盛宴。无论是 C++23 新标准的探索,还是 AI 算力优化的深度剖析,云会员都将带来一场穿越时空的知识盛宴,让每一位技术从业者尽享学习与交流的无限可能。