Skip to content

Share a precompiled header across the object libraries - #8673

Open
glebm wants to merge 1 commit into
diasurgical:masterfrom
glebm:compile-time
Open

Share a precompiled header across the object libraries#8673
glebm wants to merge 1 commit into
diasurgical:masterfrom
glebm:compile-time

Conversation

@glebm

@glebm glebm commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Adds a shared precompiled header to use with all the translation units. This reduces compilation time by about 20% across the board, measured locally and evident from CI build times here.

@glebm
glebm force-pushed the compile-time branch 4 times, most recently from c575132 to 5a15def Compare August 23, 2026 16:43
@glebm
glebm marked this pull request as ready for review August 23, 2026 18:05
Header parsing dominates our build: about 78% of the time spent compiling
Source/ goes into headers rather than into our own code, spread evenly
rather than concentrated in a few files.

Precompile the third-party headers that nearly every translation unit
pulls in. Measured with ccache disabled, this takes the full build from
1092s to 840s of CPU (-23%) for a single 2.8s PCH build.

pch.hpp deliberately contains no devilutionX headers. Including our hot
project headers as well was measured to save only a few percent more,
and would mean every edit to engine/ or utils/ invalidated the PCH and
rebuilt ~250 translation units.

The PCH is owned by libdevilutionx_pch and reused by every other object
library. That target is kept free of the optional third-party defines
(asio/sol/mpqfs) that only some libraries get: GCC accepts a PCH whose
macro state is a subset of the consumer's but rejects a superset, so
reusing libdevilutionx's own PCH fails for most of them. Executables are
excluded because they build -fPIE rather than -fPIC, which GCC also
treats as invalidating.

Sharing one PCH this way relies on the compiler tolerating a subset of
the consumer's flags, which CMake documents as unsupported and only GCC
and Clang are lenient about. MSVC reports a mismatch as an error rather
than falling back to parsing the header, so DEVILUTIONX_PCH_SHARED is
off there and libdevilutionx gets its own PCH instead. That keeps most
of the benefit: it is ~150 of the ~250 affected sources, and all of them
already compile with identical flags.

The PCH target also needs asio's include directories. GCC does not check
include paths when validating a precompiled header, so a difference
there is silent: the PCH bakes in one resolution of a header and the
consumer can no longer reach the other. 3DS and Switch shadow <errno.h>
with a shim on asio's include path to add ESHUTDOWN, which their newlib
only defines under __LINUX_ERRNO_EXTENSIONS__, and pch.hpp reaches
<errno.h> via <ostream>. Only the include directories: linking asio
would add its compile definitions to the PCH and consumers without them
would then reject it.

USE_SDL1 and USE_SDL3 move to DEVILUTIONX_DEFINITIONS for the same reason,
in the other direction: the PCH links DevilutionX::SDL and is built with
the macro, and taking the include directories alone is not an option
because pch.hpp needs the macro to decide which SDL header to include.
Defining it everywhere instead of only where SDL is linked keeps the PCH
valid for the 16 leaf libraries that do not link SDL. Over their 17
sources a rejected PCH measures 22.4s against 14.2s without one, and it
is silent: a target that was never given a PCH is not compiled with
-Winvalid-pch either. Compiling them with the macro produces
byte-identical objects, so nothing but PCH validity changes.
DevilutionX::SDL still defines it for the targets that are not built
through add_devilutionx_library(), such as the tests. SDL2 defines no
such macro and was never affected.

The precompiled header is C++, so sources in other languages are marked
SKIP_PRECOMPILE_HEADERS. Amiga adds C sources to libdevilutionx and iOS
adds an Objective-C one, and without this CMake looks for a C
precompiled header that no target builds and fails to generate.

The sources that raise the optimization level to stay usable in Debug
builds opt out too. A precompiled header is only valid for a translation
unit compiled at the same optimization level: GCC ignores it with a
-Winvalid-pch warning, Clang fails the build outright. This gives up
~3.3s in optimized builds, where the levels do match, but
SKIP_PRECOMPILE_HEADERS is evaluated at generate time and so cannot be
made per-configuration.

The third-party includes are __has_include-guarded. The owning target
always has them available, so the precompiled image is complete; the
guards only matter if a toolchain ever rejects the PCH, so that the
fallback re-parse degrades to a slower build rather than a broken one in
a target that lacks those include directories.

ccache needs pch_defines,time_macros sloppiness to hash a PCH at all,
and it has to reach ccache at build time, hence the launcher wrapper.
Without it ccache misses on every PCH-using compile.

Set DEVILUTIONX_PCH=OFF to disable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@glebm
glebm enabled auto-merge (rebase) August 24, 2026 10:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant