This post introduces two ways to use the std module implicitly without modifying the source code, and tests them on the Seastar project.

The first is the Preload approach. It makes the compiler load the std module before compiling each translation unit. This is roughly equivalent to adding import std; at the beginning of every .cpp file.

The second approach uses Clang’s Module Map feature. It makes the compiler automatically replace #include <STL headers> with import std;.

Both approaches are covered in more detail in the official Clang documentation.

For this experiment, I ran the baseline (Headers), Preload, and Module Map builds three times each and used the median result. All tests used -j8. Wall is the total build time with parallel compilation. User is the total user-mode CPU time across all threads. Max RSS is the peak memory usage during compilation.

Here are the results:

  Baseline (Headers) 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 Median 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 Median 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 Median 700.97 MiB 719.92 MiB 677.58 MiB

Compared with the 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%

Compared with Preload, Module Map:

  • Reduced wall time by 9.54 seconds, or 11.76%.
  • Reduced user time by 71.37 seconds, or 12.55%.
  • Reduced Max RSS by 42.34 MiB, or 5.88%.

Overall, the results look quite good.

One thing I noticed is that Preload uses more memory than the baseline. For example, compiling

import std;
#include <string>

uses more memory than compiling

#include <string>

This seems intuitive to me.

Toolchain

  • CMake 4.4.3.
  • Clang built from the 24.git source. I found a hang issue that may occur when compiling Seastar with the Preload approach using Clang 23 or earlier.
  • Standard library: libc++ built from the 24.git source.

Implementation Details

The complete patch is available at https://github.com/ChuanqiXu9/seastar/tree/use_std_module_implicitly.

First, I added a file containing only import std;:

// ImportStd.cpp
import std;

I then created a library for it in CMake and linked the other Seastar targets against it:

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 ()

The purpose is to let CMake bring in the BMI for the std module.

One important detail: the seastar_import_std target must be built first. Otherwise, CMake will not set up the dependency correctly.

For Preload, we only need to add -fmodule-file=<path-to-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 ()

For Module Map, I added the following module map file:

// 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"
}

This file should also be reusable in other projects. I left out headers such as cerrno and cassert because they expose macros, and C++20 named modules do not export macros.

At the moment, the headers referenced by std.modulemap cannot be linked directly to the standard library headers. To work around this, I created a directory during the build and added symlinks to the standard library headers so that std.modulemap could find them:

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

Then I specified the module map path in CMake:

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 ()

The VFS approach described in the official documentation can also be used.

Apart from that, I only needed to add a few headers that had previously been included indirectly and fix one name conflict:

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) {

This happens because a module map effectively replaces #include, so it can change the program’s behavior.

Summary

At the language level, the Preload approach should cause almost no behavior changes, but it may trigger more compiler bugs. It is also fairly easy to set up.

The Module Map approach takes more work and may cause compilation errors due to macros and name conflicts. However, it gives the compiler a cleaner view of the code, is less likely to trigger compiler bugs, and provides a clear improvement in build time.

This post is meant to share a few useful tricks rather than recommend a best practice. These approaches need better build-system support before they can be used smoothly in real projects. For now, the setup is mainly useful for simple experiments.