首页 » 科学 » C++ 开拓者怒了:这个无用的模块设计最终会害去世 C++!_模块_文件

C++ 开拓者怒了:这个无用的模块设计最终会害去世 C++!_模块_文件

萌界大人物 2024-12-19 08:09:36 0

扫一扫用手机浏览

文章目录 [+]

C++20

中。
个中,Modules 便是可能进入 C++ 20 的一大主要特性:“一贯以来 C++ 一贯通过引用头文件办法利用库,而其他90年代往后的措辞比如 Java、C#、Go 等措辞都是通过 import 包的办法来利用库。
现在 C++ 决定改变这种情形了,在 C++20 中将引入 Modules,它和 Java、Go 等措辞的包的观点是类似的,直接通过 import 包来利用库,再也看不到头文件了。

C++ 开拓者怒了:这个无用的模块设计最终会害去世 C++!_模块_文件 C++ 开拓者怒了:这个无用的模块设计最终会害去世 C++!_模块_文件 科学

然而便是这一特性,前段韶光在 Twitter 上引发了不小的谈论。
再加上诸多其他问题,“

C++ 开拓者怒了:这个无用的模块设计最终会害去世 C++!_模块_文件 C++ 开拓者怒了:这个无用的模块设计最终会害去世 C++!_模块_文件 科学
(图片来自网络侵删)

C++ 20 还未发布就已凉凉

”的论调也早有苗头。
C++ 模块化,究竟是问题多多的无用考试测验,还是如期待般能带来其承诺的性能升级呢?

作者 | vector-of-bool

译者 | 苏本如

责编 | 仲培艺

出品 | CSDN(ID:CSDNNews)

以下为译文:

C++ Modules(模块化)被视作 C++ 自出身以来最大的变革,其设计有几个基本目标:

1. 自顶向下隔离:模块的“导入程序”不能影响正在导入的模块的内容。
导入源中编译器(预处理器)的状态与导入代码的处理无关。

2. 自下而上隔离:模块的内容不会影响导入代码中预处理器的状态。

3. 横向隔离:如果两个模块由同一个文件导入,则它们之间不会“串扰”。
导入语句的顺序无关紧要。

4. 物理封装:只有模块显式声明为导出的实体才会对利用者可见。
模块中未导出的实体不会影响其他模块中的名称查找(除了 ADL 可能有一些不同之处【依赖实参的名字查找】,但这就说来话长了)。

5. 模块化接口:逼迫任何给定模块的公共接口在称为“模块接口单元”(MIU)的单个 TU 中声明。
模块接口子集的实现可以在称为“分区”的不同 TU 中定义。

如果你期望 Modules 可以像 C++ 的许多其它功能一样耐久不衰,那么你会把稳到上面这个列表中短缺了“编译速率”。
然而,这是 C++ Modules 模块最大的承诺之一。
模块带来的速率提升可能便是归功于上面的设计。

下面我列出从 Modules 设计中受益匪浅的 C++ 编译的几个方面,按照从最明显到最不明显的顺序:

1. 标记化缓存(Tokenization Caching):由于 TU 的隔离,当模块后面导入另一个 TU 时,可以缓存已经标记化的 TU。

2. 解析树缓存(Parse-tree Caching):和标记化缓存一样。
标记化和解析是 C++ 编译中开销最大的操作之一。
我自己的测试显示,对付具有大量预处理输出的文件,解析可能会占用高达 30% 的编译韶光。

3. 延迟重编译(Lazy Re-generation):如果 foo 导入了bar,然后我们修正了 bar 的实现,我们可以不须要对 foo 立即重新编译。
只有对 bar 接口修正后才须要重新编译 foo。

4. 模板专门化:这一点比较奇妙,可能须要更多的事情来实现,但潜在的加速是巨大的。
简而言之,模块接口单元中涌现的类或函数模板在经由专门化处理后可以在磁盘上缓存并供后续须要时加载。

5. 内联函数代码复制缓存:内联函数(包括函数模板和类模板的成员函数)的代码复制结果可以缓存,然后由编译器后端重新加载。

6. 内联函数省略代码复制:extern template 许可编译器省略对函数和类模板实行代码复制,这对编辑器的代码去重操作非常有益。
模块许可编译器隐式实行更多的 extern template-style 优化。

看上去模块设计相称不错,不是吗?

但是我们都忽略了一个非常恐怖且极为糟糕的毛病。

还记得…… Fortran 吗?

FORTRAN 实现了与 C++ 的设计有点相似的模块系统。
几个月前,SG15 工具研究小组在圣地亚哥提交了一篇文章(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1300r0.pdf),据我所知,这篇文章迄今为止没有得到任何干系人士的谈论和评论。

文章要点摘录如下:

1. 我们有模块 foo 和 bar,分别由 foo.cpp 和 bar.cpp 定义。

2. bar.cpp 里有 import foo; 语句。

3. 在编译 bar.cpp 时,如何确保 import foo 被解析?当前的设计和实现有一个为 foo 定义的所谓“二进制模块接口”(简称BMI)。
这个 BMI 是文件系统中描述模块 foo 导出接口的文件。
我就叫它 foo.bmi, 文件扩展名在这里无所谓。

4. foo.bmi 是编译 foo.cpp 的副产品。
编译 foo.cpp 时,编译器将天生 foo.o 和 foo.bmi。
因此,必须在 bar.cpp 之前编译 foo.cpp!

趁着警铃还没有拉响,我们来谈论一下我们目前利用头文件的事情办法:

1. 我们有一个模块 foo,由 foo.cpp 和 foo.hpp 定义; 和另一个模块 bar,由 bar.cpp 和 bar.hpp 定义。

2. bar.cpp 中有 #include <foo.hpp>。

3. 在编译 bar.cpp 时,如何确保 #include<foo.hpp> 被解析?这很大略:确保 foo.hpp 存在于 header 搜索路径列表的目录中。
我们不须要做任何额外的预处理。

4. 对模块 foo 和 bar 的编译没有次序哀求,可以并行处理。

并行化可能是提高 build 性能最主要的方面。
优化 build 时,你无需再考虑并行化,由于它已经存在了。

模块改变了这一点。
模块的导入导致了一个编译韶光的依赖项,这在 #include 语句中并没有表示。
(关于模块编译的次序问题,可参考:https://vector-of-bool.github.io/2018/12/20/build-like-ninja-1.html)。

Rene Rivera 最近在《Are modules fast?》(https://bfgroup.github.io/cpp_tooling_stats/modules/modules_perf_D1441R1.html)一文中磋商了这种设计的后果。

剧透一下 Rene 文章的结论:答案是否定的,或者更准确一点来讲,这很奇妙,但大多数情形下答案仍旧是不。
这篇文章中利用确当前模块实现是非常原始的,但仍旧在理解哪些模块看上去对性能有帮助这方面有一定的参考代价。
可以期待,随着硬件并行性的提升,header 的勾引模块变得越来越主要,而且与 DAG 深度(即相互导入的模块链的长度)也有关系。
随着 DAG 深度的增加,模块会越来越慢,而 header 则保持相称稳定,纵然是对付靠近 300 的“极度”深度。

一个徒劳的扫描任务

假设我有下面的源文件:

import greetings;import std.iostream;int main() { std::cout << greeting::english() << '\n';}

这很大略。
由于我们导入了一些模块,以是我们须要先编译 greetings 和 std.iostream,然后才能编译这个文件。

那么,让我们来……

emmm……

怎么啦?

我们只有一个包含两个 import 的源文件,仅此而已,别无他物。
我们不知道 greetings 是在哪里定义的,我们须要找到这个包含 module greetings; 语句的文件。

在银河系另一侧的 talk.cpp 文件看起来很可能是:

module;#ifdef FROMBULATE#include <hello.h>#endif#ifndef ABSYNTHexport module something.pie;#endifimport std.string;export namespace greeting {std::string english();}

它定义了我们想要的 greeting::english 函数。
但是我们怎么知道这是精确的文件呢?它并没有 module greetings; 这一行!

但它某些时候确实是我们要的。
当我们利用 -DFROMBULATE 编译时,文件 hello.h 会被粘贴到源文件中。
让我们看看 hello.h 里面有什么?

#ifdef __SOME_BUILTIN_MACRO__# define MODULE_NAME greetings#else // Legacy module name# define MODULE_NAME salutations#endifexport module MODULE_NAME;

Oh no!

好吧好吧……别担心。
我们须要做的便是……运行预处理器来检讨文件中是否涌现 module salutations 或 module greetings。

这是可以的,但是有 4201 个文件可以定义可以被导入的模块,个中任何一个都可能有 module greetings;。

其余,我们还不能利用自己的预处理器实现,须要精确地运行编译这段代码的预处理器。
看到 __SOME_BUILTIN_MACRO__ 了吗?我们不知道那是什么。
如果我们没有精确地对它进行编译,编译就会失落败。
更糟的是,我们乃至可能会缺点地编译此文件。

那么我们能做什么呢?我们可以在预处理完所有文件后缓存所有模块的名称,对吗?那么,我们在哪里存储这个映射表呢?当我们想用一个不同的编译器编译,天生不同的映射表时会发生什么?如果我们添加须要扫描的新文件怎么办?为了检讨任何模块是否添加、删除或重命名了,我们是否须要在每次构建时搜索这些包含了数千个源文件的所有目录?在那些启动进程和/或访问文件须要较大开销的系统上,这些本钱也将会叠加上去。

可能的办理方案

这两个问题虽然不同,但却是干系的,我(和许多其他人)认为模块设计的一个改变可以办理这两个问题, 那便是模块接口单元的位置必须是确定的。

有两种备选方案可以履行:

1. 逼迫从模块名称派生 MIU 文件名。
这仿照了头文件名的设计,它与如何从 #include 指令中找到头文件名直接干系。

2. 供应一个“manifest”或“mapping”文件,描述基于模块名的 MIU 文件路径。
此文件须要用户供应,否则我们将同样碰着上文描述的扫描问题。

有了确定且易于定义的 MIU lookup(查询),我们就可以进入下一个必要步骤:必须延迟天生模块的 BMI。

TU 之间的编译顺序将扼杀 module adoption 的进程。
纵然是相对较浅的 DAG 深度也比与头文件相同的深度慢得多。
唯一的答案是 TU 编译必须是可并行的,纵然是导入其他 TU 时。

在这方面,C++ 最好模拟 Python 的导入实现:当碰着新的导入语句时,Python 将首先找到对应于该模块的源文件,然后以确定性的办法查找预编译的版本。
如果预编译版本已经存在并且是最新的,就利用它;如果不存在预编译版本,则将编译源文件,并将天生的字节码写入磁盘。
然后加载此字节码。
如果两个阐明器实例同时碰着同一个未编译的源文件,它们将竞争写字节码。
不过,竞争并不主要,它们都会得出相同的结论,并将相同的文件写入磁盘。

为了方便 DAG 中 TU 的并行编译,C++ 模块必须以相同的办法实现。
提前编译 BMI 是不可能的。
相反,当编译器第一次碰着有关模块的 import 语句时,该当延时天生 BMI。
Build 系统根本不应该与 BMI 有关。

只有当一个 MIU 的位置对付编译器是确定的时候,以上这些才能实现。

前景渺茫

前段韶光,Twitter 上发生的事让民气乱如麻。
Kona 会议前的邮件列表在 1 月 25 日开放了。
在发布的许多文章中,有一篇《关注模块的工具能力(Concerns about module toolability)》(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1427r0.pdf),其作者和贡献者名单中很多是来自业界的系统和工具构建工程师。
我想呼吁威信人士的关注,但我以为这份名单中的人才是最有资格供应 module toolability 反馈的人。

这篇文章的出身源于许多工具作者和互助者(并不局限于论文中所提及的,包括我自己)的关注,由于大家都深深感到自己长久以来对付模块的关注都被忽略了。

SG15 之外的人一贯热衷于回嘴关于 module toolability 问题的谈论,他们声称 SG15 缺少必要的实现履历,无法对模块这个话题提出有用的建议。

SG15 只搞过面对面的会议,上次在圣地亚哥的会议也没起到什么浸染,由于主席不在,而且大家急急忙忙参会,没韶光进行任何有用的谈论。
由于在官方的 WG21 会议之外没有安排 SG15 会议,因此其成员很难担保更新并协同事情。
此外,SG15 曾多次考试测验重提已经被谢绝的问题,被谢绝的缘故原由是由于他们提出的问题被认为“超出了 C++ 措辞范围”。

关于 Kona 会议前邮件列表的推文催生了关于 C++ 模块化的谈论:关于 module toolability,该相信谁?(https://twitter.com/horenmar_ctu/status/1089542882783084549)。

这场谈论终极以哀求 SG15 “他妈的闭嘴”而告终,除非 SG15 能够供应代码示例来证明它们所提到的问题。
但是这个示例代码,无法在当前的任何编译器中实现,也不能在任何当前的构建系统中实现。
以是纵然这些问题确实存在,这个哀求也只能得出一个否定的结论,由于这是一个无法凭履历完成的任务。
也便是说,哀求 SG15 供应代码根本是一个无法永久完成的任务。

这些问题没有连续谈论下去,也没有被推翻。
乃至没有人再提到 《关注模块的工具能力》中列出的问题。
我们只是被大略地奉告要相信一些大人物比我们更理解 C++ 模块(这里我要再次呼吁威信人士参与)。

支持目前模块设计的人尚未证明模块能适应大规模生产环境,但是他们却哀求 SG15 供应模块不能知足大规模生产的证据。
只管已有的模块支配并没有利用当前的设计,也没有利用真实环境中构建实际系统所需的自动模块扫描。

如果模块被合并,结果创造它们不能以良好的性能和灵巧的办法实现,那么人们就不会利用模块。
如果一个 broken module 建议被合并到 C++ 中,后果可能是不可填补规复的,C++ 也将永久得不到模块设计承诺带来的好处。

至于针对当前模块设计的改进方案能成功办理这些问题呢?我不能给出确定的答案,但我和许多人都认为 C++ Modules 有重大问题须要办理。

然而,从其他人的做法来看,SG15 怎么想彷佛并不主要,他们的发起总是被缺少 C++ 工具履历的人反对, 他们在全体谈论中没有任何发言权,提出的任何问题都被认定为“未经证明”和“超出范围”而不予考虑。

我不太敢责怪这种行为的后果,我也并不热衷“人际冲突”。
然而,我更担心 C++ 这个无用的模块设计终极会害去世自己。

原文:https://vector-of-bool.github.io/2019/01/27/modules-doa.html

本文为 CSDN 翻译,如需转载,请注明来源出处。
作者独立不雅观点,不代表 CSDN 态度。

标签:

相关文章

桌面软件语言的演变与未来发展

随着科技的飞速发展,计算机技术逐渐渗透到我们生活的方方面面。其中,桌面软件作为计算机技术的重要组成部分,其语言的发展历程也颇具代表...

科学 2025-01-02 阅读0 评论0

核心网协议栈,通信领域的基石

随着信息技术的飞速发展,通信领域不断涌现出各种新技术、新应用。作为通信网络的“大脑”,核心网协议栈在保障通信质量和安全方面发挥着至...

科学 2025-01-02 阅读0 评论0