Building LLVM is slow, this is a frequent complaint, especially from people with weaker hardware. Build times are dominated by the C++ front-end, (repeatedly) parsing headers is the most time consuming part. C++ module builds don’t really help (for me), are fragile, and kill parallelism.
Therefore, I propose to extensively, but optionally, use pre-compiled headers (rough draft PR) for frequently used headers (i.e., C++ stdlib + Support, IR, CodeGen, gtest). This can substantially reduce build times of LLVM by ~1.5-2x, front-end time by ~3x (see breakdown below). stage2-clang build times improve by ~40% on c-t-t. On my laptop (M2 MacBook Air, 4+4 cores), a dual-target LLVM build now just takes 6-7 minutes (incl. tools), unit tests add another 2.5 min.
Note that pre-compiled headers are already used in-tree by Flang for some libraries to speed up compilation (e.g. here). This proposal implements a much more extensive approach, where almost all libraries would use PCHs. Further build time improvements are possible by using more different PCHs for other libraries like clangAST and dependents.
Downsides: using PCHs has two caveats:
- Regular build fails but PCH build succeeds: PCH masks missing includes.
- Regular build succeeds but PCH build fails: much more “used” includes can cause naming collisions (e.g. llvm::Reloc from llvm/Support/CodeGen.h vs. lld::macho::Reloc from lld/MachO/Relocations.h; where e.g. lld/MachO/InputSections.cpp uses both namespaces) and (rarely) ambiguities due to more available implicit conversions.
We’d therefore need CI (at least post-commit) that regularly tests PCH and non-PCH builds to make sure that both builds work.
General Questions
- Does this extensive PCH use has a reasonable chance of getting merged upstream? (If not, I’d not spend more time in polishing this into a mergeable state.)
- Enable by default vs. not?
- Flang already uses PCH – what are experiences worth noting?
Details: Implementation (+Technical Questions)
The current patch builds four pre-compiled headers. This is “somewhat arbitrary” in that I simply selected the headers that show up with long parse times accumulated over all CUs.
- LLVMSupport, which includes all C++ standard headers and frequently used headers from llvm/Support and llvm/ADT.
- LLVMCore, which extends LLVMSupport with frequently used headers from llvm/IR.
- LLVMCodeGen, which extends LLVMCore with frequently used headers from llvm/CodeGen.
- clangAST, which extends LLVMSupport with some headers from clang/AST.
- llvm_gtest, which extends LLVMSupport with gtest/gtest.h.
Libraries that depend on CodeGen reuse the LLVMCodeGen PCH, depend on Core but not CodeGen reuse the LLVMCore PCH, depend on Support but not Core reuse the LLVMSupport PCH.
I currently put the header list in include/llvm//pch.h. Not the best place (we might not want them installed), but lib/ is also not ideal, as e.g. IR/pch.h would include “../Support/pch.h”, which also feels wrong. We could also keep them in CMakeLists.txt, but that’d make reuse of the list more awkwards (e.g., extending the list from LLVMSupport to LLVMCore, which should be a superset).
- Where to store header list for PCH?
- Add separate option to enable/disable vs. just rely on standard
CMAKE_DISABLE_PRECOMPILE_HEADERS? - Which CI runners use PCH vs. which don’t? (NB: ccache supports PCH, sccache apparently doesn’t.)
How to not hard code the PCH selection in AddLLVM.cmake?(replaced with simple dependency chain length heuristic)There’s a minor perf regression in PCH builds, which I haven’t yet investigaged – any ideas why this could be? (I’d have expected the output to be nearly identical.)(I accidentally included iostream…)- Two unittests (flang/unittests/Evaluate, clang/unittests/Interpreter/ExceptionTests) use exceptions and also enable RTTI. To build the llvm_gtest PCH without RTTI, I changed the llvm_gtest build to not forcefully enable RTTI. Are there expectable problems from building these two tests with
-fexceptions -fno-rtti? (works for me) (Tangentially related: what is the reason why-fno-exceptions -funwind-tablesshow up inllvm-config --cxxflags? Using exceptions when not unwinding through LLVM should be fine, so only-fno-rttishould be there?) - If we want PCH by default, on which platforms? There appear to be problems with the Flang PCHs on Windows.
(Note to self: the Flang-specific parts here need to be moved to AddLLVM)(done)
Details: Data
Time breakdown (seconds; collected with -ftime-trace) with a X86+AArch64 -O1 build on a 48-core machine:
main +PCH
--------------------------------- ----- ----
ExecuteCompiler 11528 5653
Frontend 9128 3388
Source 6326 1488
PerformPendingInstantiations 2160 1124
CodeGen Function 287 329
Backend 2333 2201
Optimizer 1539 1454
CodeGenPasses 786 741
wall time (48c/96t) 139 86
CPU time (usr+sys) 11706 5919
cmake -DLLVM_TARGETS_TO_BUILD="X86;AArch64" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS_RELEASE=-O1 -DCMAKE_CXX_FLAGS_RELEASE=-O1 -G Ninja -DLLVM_ENABLE_ASSERTIONS=ON -DLLVM_LINK_LLVM_DYLIB=ON -DCMAKE_C_COMPILER=.../clang -DCMAKE_CXX_COMPILER=.../clang++ -DCMAKE_C_FLAGS="-ftime-trace -ftime-trace-granularity=100" -DCMAKE_CXX_FLAGS="-ftime-trace -ftime-trace-granularity=100" -DLLVM_USE_LINKER=lld -B llvm-build && ninja -C llvm-build
NB: due to the high parallelism on this machine, the length of the slowest compile units becomes increasingly relevant, e.g. SLPVectorize.cpp takes >40s alone. Also note: using TPDE-LLVM as back-end can replace the time in CodeGenPasses with ~10s, but this will continue to be my local setup. ![]()
File size of the precompiled headers; total build directory size grows from 961 MiB to 1224 MiB with the config from above:
105M .../LLVMCodeGen.dir/cmake_pch.hxx.pch
69M .../LLVMCore.dir/cmake_pch.hxx.pch
39M .../LLVMSupport.dir/cmake_pch.hxx.pch
43M .../llvm_gtest.dir/cmake_pch.hxx.pch
Alternatives
- Make Clang 3x faster. I see no fundamental reason why parsing C++ has to be this slow, but this is very unlikely to happen (I’m not going to write a new C++ parser and I believe Clang still has the trend of becoming slower over time).
- Restructure code so that much fewer includes are needed. Lot of effort, unlikely to happen. Might also only have limited effect, because several standard library headers are very slow to parse.
- Use unity/jumbo builds (several .cpp files are compiled together). This reduces the number of times headers are parsed, but also reduces parallelism, increases memory usage, and increases the cost of incremental builds. (thanks, @makslevental)
- Use C++20 modules. This would require substantial refactoring and is unlikely to be feasible for the near/medium future due to our toolchain requirements. (thanks, @h-vetinari)
- Use Clang header modules (
cmake -DLLVM_ENABLE_MODULES=ON). This substantially reduces build parallelism, is incompatible with libstdc++, and appears to be rather fragile with unclear/varying build time benefits. - Rewrite LLVM in a language that compiles faster… haha, just kidding.
- Do nothing.