两个隐式使用 std module 的方法及简单测量
这篇文章简单介绍两个隐式使用 std module 的方法而不需要显式修改源代码,并以 seastar 项目进行了测试。
第一个方法称之为 Preload 方法,即让编译器对每个 TU 都先加载 std module 再进行后续编译。这种方法近似于在每个 .cpp 文件开头插入一个 import std;
第二个方法使用了 Clang 的 Module Map 特性,让编译器自动把 #include <STL headers> 替换为 import std;
两个方法在 Clang 官方文档中都有更完整的提及。
本次实验分别跑了 baseline(Headers)、Preload 和 Module Map 各三遍,然后取中位数。测试时使用 -j8。Wall 是并发下的总编译时间。User 是所有线程的
用户态时间总和。Max RSS 是编译时最大内存消耗量。以下为测试数据:
| Baseline(Header) | Preload | Module Map | |
|---|---|---|---|
| Wall | 134.06 / 91.77 / 92.21 s | 81.09 / 81.32 / 80.44 s | 71.55 / 72.22 / 71.28 s |
| Wall Mid | 92.21 s | 81.09 s | 71.55 s |
| 三次 User | 940.54 / 655.39 / 655.70 s | 568.83 / 569.76 / 564.11 s | 495.90 / 500.87 / 497.46 s |
| User Mid | 655.70 s | 568.83 s | 497.46 s |
| Max RSS | 699.98 / 702.75 / 700.97 MiB | 719.92 / 719.97 / 719.51 MiB | 677.58 / 679.11 / 677.25 MiB |
| Max RSS Mid | 700.97 MiB | 719.92 MiB | 677.58 MiB |
相对 Baseline 的结果是
| Preload | Preload + module map | |
|---|---|---|
| Wall Change | -11.12 s,-12.06% | -20.66 s,-22.41% |
| Wall Speedup | 1.137x | 1.289x |
| User Change | -86.87 s,-13.25% | -158.24 s,-24.13% |
| Sys Change | -1.49 s,-6.74% | -5.23 s,-23.65% |
| Max RSS Change | +18.95 MiB,+2.70% | -23.39 MiB,-3.34% |
Module Map 相比于 Preload:
- Wall time 下降 9.54 秒,11.76%
- User time 下降 71.37 秒,12.55%
- Max RSS 下降 42.34 MiB,5.88%
整体数据比较漂亮。
这里观察到 Preload 时使用的内存开销大于 Baseline,比如
import std;
#include <string>
的编译时内存开销大于
#include <string>
我觉得也符合直觉。
工具链
- Cmake 使用的是 4.4.3。
- Clang 使用的是 24.git 源码构建的版本,这里发现了 23 及之前版本使用 Preload 方法编译 seastar 时可能触发的一个超时问题。
- 标准库:libcxx 24.git 源码构建的版本。
改造细节
完整的 Patch 在 https://github.com/ChuanqiXu9/seastar/tree/use_std_module_implicitly
首先我引入了一个只包含 import std; 的文件
// ImportStd.cpp
import std;
并在 CMake 中为其添加一个库,并让其他 seastar 目标链接它
add_library (seastar_import_std STATIC cmake/ImportStd.cpp)
foreach (target IN LISTS seastar_buildsystem_targets)
if (target STREQUAL "seastar_import_std")
continue ()
endif ()
add_dependencies (${target} seastar_import_std)
set_property (TARGET ${target} APPEND PROPERTY
LINK_LIBRARIES seastar_import_std)
endforeach ()
这里的目的是让 CMake 帮我们引入 std module 的 BMI。
这里需要强调,构建时必须先构建 seastar_import_std 目标,不然 Cmake 不会正确的建立依赖。
然后对于 Preload,我们只需要添加 -fmodule-file=<path-std-BMI> 即可
if (Seastar_PRELOAD_STD_MODULE)
target_compile_options (${target}
PRIVATE
"$<$<COMPILE_LANGUAGE:CXX>:-Wno-eager-load-cxx-named-modules>"
"$<$<COMPILE_LANGUAGE:CXX>:-fmodule-file=${Seastar_STD_MODULE_BMI}>")
endif ()
然后对于 ModuleMap,我们添加以下 module map 文件:
// std.modulemap
module std [system] {
requires cplusplus
header "algorithm"
header "bitset"
header "complex"
header "deque"
header "exception"
header "fstream"
header "functional"
header "iomanip"
header "ios"
header "iosfwd"
header "iostream"
header "istream"
header "iterator"
header "limits"
header "list"
header "locale"
header "map"
header "memory"
header "new"
header "numeric"
header "ostream"
header "queue"
header "set"
header "sstream"
header "stack"
header "stdexcept"
header "streambuf"
header "string"
header "typeinfo"
header "utility"
header "valarray"
header "vector"
header "array"
header "atomic"
header "chrono"
header "codecvt"
header "condition_variable"
header "forward_list"
header "future"
header "initializer_list"
header "mutex"
header "ratio"
header "regex"
header "scoped_allocator"
header "system_error"
header "thread"
header "tuple"
header "typeindex"
header "unordered_map"
header "unordered_set"
header "optional"
header "any"
header "variant"
}
这个文件在其他项目中应该也可以复用。这里的细节是我把 cerrno cassert 等暴露宏的头文件去掉了,因为 C++20 Named Module 不导出宏。
注意这里 std.modulemap 里引用的头文件还不能和标准库头文件关联起来,所以我在构建时创建了一个目录,在其中创建标准库头文件的软链接,来让
std.modulemap 找到对应的头文件
mkdir -p std-module-map
cp std.modulemap std-module-map/.
cd std-module-map
libcxx_dir=...
awk -F '"' '/^[[:space:]]*header[[:space:]]+"/ {print $2}' std.modulemap |
while IFS= read -r header; do
ln -s "$libcxx_dir/$header" "$header"
done
然后在 cmake 里指定 module map 路径即可
if (Seastar_STD_MODULE_MAP)
if (NOT EXISTS "${Seastar_STD_MODULE_MAP}")
message (FATAL_ERROR
"Standard module map does not exist: ${Seastar_STD_MODULE_MAP}")
endif ()
target_compile_options (${target}
PRIVATE
"$<$<COMPILE_LANGUAGE:CXX>:-fmodule-map-file=${Seastar_STD_MODULE_MAP}>")
endif ()
此外也可以使用官方文档中使用的 vfs 方法。
此外就只有很简单的补足隐式包含的头文件和冲突的命名了:
diff --git a/include/seastar/core/posix.hh b/include/seastar/core/posix.hh
index 8dd85b34..4bd734ef 100644
--- a/include/seastar/core/posix.hh
+++ b/include/seastar/core/posix.hh
@@ -36,6 +36,7 @@
#include <signal.h>
#include <spawn.h>
#include <unistd.h>
+#include <cerrno>
#include <utility>
#include <system_error>
#include <chrono>
diff --git a/src/net/tls-impl.cc b/src/net/tls-impl.cc
index 12e19a7e..3524b716 100644
--- a/src/net/tls-impl.cc
+++ b/src/net/tls-impl.cc
@@ -463,7 +463,6 @@ class tls::reloadable_credentials_base {
, _delay(delay)
{}
future<> init() {
- std::vector<future<>> futures;
visit_blobs(_blobs, make_visitor(
[&](const std::string_view&, const x509_simple& info) {
_all_files.emplace(info.file.filename);
@@ -485,7 +484,7 @@ class tls::reloadable_credentials_base {
}
void start() {
// run the loop in a thread. makes code almost readable.
- (void)async(std::bind(&reloading_builder::run, this)).finally([me = shared_from_this()] {});
+ (void)seastar::async(std::bind(&reloading_builder::run, this)).finally([me = shared_from_this()] {});
}
void run() {
while (_creds) {
这是因为 module map 本质是替换 #include,所以会改变行为。
总结
Preload 方法在标准层面(几乎)不会修改行为,但可能会触发更多编译器 bug。Preload 方法的引入也比较简单。
Module Map 方法引入步骤更复杂,也可以导致一些编译错误(因为宏和命名冲突)。但对编译器来说更干净,更不会引入 bug,对编译加速的幅度也很明显。
这里的分享更多是介绍一些 trick,而不是最佳实践。因为这里的方法需要构建系统的介入才能在真实世界里足够丝滑的用起来。现在的做法只足够做一些简单的实验。