The time and/or memory requirements to build Flang are an often heard, not only by new users. Examples:
- RFC: F18 build time memory requirements are too high
- Debug build of FortranSemantics.lib too large · Issue #125749 · llvm/llvm-project · GitHub
- Running out of memory building flang
- Build Time Comparision
- Fail to build flang-new (fa8dc363) on Raspberry Pi 5: `-Werror=restrict` - #2 by Leporacanthicus
- flang mingw-w64 build fails with string table overflow, file too big · Issue #63582 · llvm/llvm-project · GitHub
- 32-bit builds are impossible due to the memory usage
- Fatal Error while building LLVM
- [flang] include flang by default under LLVM_ENABLE_PROJECTS · Issue #112789 · llvm/llvm-project · GitHub
- [flang] build regression: builds in Azure Pipelines run out of memory despite 16GB swapfile · Issue #117814 · llvm/llvm-project · GitHub
- [CI] Disable Flang from pre-commit tests when Flang files are not touched on Windows Only by joker-eph · Pull Request #93729 · llvm/llvm-project · GitHub
- LLVM 20 ninja Build Failing on WSL
- Flang’s GettingStarted guide once recommended compiling Flang with
make -j[1] - 953484 – sys-devel/gcc-14.2.1_p20250301: absurdly high memory usage when compiling llvm-core/flang
- [Issue]: Out of Memory(OOM) issue during amd-llvm flang build phase · Issue #1544 · ROCm/TheRock · GitHub
The number of compile Flang build jobs can limited with FLANG_PARALLEL_COMPILE_JOBS. However, it is off by default, only supported by Ninja, and users do not really want to limit parallelism.
The reason main reason is Flang’s reliance on templates. I identified two patterns that I think contributes to the majority of the unusual build cost:
- std::variant polymorphism: Instead of class inheritance such as
class Superclass { virtual void func(); };
class DerivedA : public Superclass {};
class DerivedB : public Superclass {};
obj->func();
Flang uses:
class DerivedA { void func(); } ;
class DerivedB { void func(); };
template<>
class Superclass {
UNION_CLASS_BOILERPLATE(Superclass)
std::variant<DerivedA, DerivedB> u;
};
std::visit([](const auto &v) {
v.func();
}, obj.u);
There are three big hierarchies built like that, each have slightly different design: the parse tree, the evaluate tree, and expressions/operations.
- Template arguments replacing immutable fields. Instead of what Clang does
class Expr : public ValueStmt {
QualType TR; // Return type
template <typename DERIVED, typename RESULT, typename... OPERANDS>
class Operation;
...
template <TypeCategory CAT, int KIND>
class Expr<Type<CAT,KIND>> { // Pseudocode, these are template specializations per CAT
std::variant<...> u; // Possible operations/constants/... returning value of this CAT
That is, Operations (such as Subtract) are a CRTP[2] instantiated for each possible combination of result and operand types.
Expr are an object with a nested std::variant of possible operations (or constant), templated by the Type of the result.
Flang has 7 type categories each with a set of supported kinds (e.g. INTEGER supports the kinds 1, 2, 4, 8, 16 corresponding to the width in bytes), which makes in total 20 Types + dynamic ones. Fortunately only the conversion operation can have the result and operand types (FROM, TO) be chosen combinatorially and call arguments are modelled using dynamic types. That still means 20 instantiations for each expression supported by Flang. And most instantiated classes of a different KIND do exactly the same. One may think of it as storing the KIND as part of a function pointer, but without masking out the KIND part so we have a copy of the code for each of them.
I did some measurements compilation time using gcc and Clang, the latter with ClangBuildAnalyzer. This is a standalone build of Flang with ninja all, i.e. the LLVM/MLIR libraries and Clang are not part of the build. Also, I measured the compile time of the 3 Fortran-only applications in SPEC 2017 (603.bwaves_s, 649.fotonik3d_s, 654.roms_s), using the gcc-compiled flang. This runs on a 512 thread, 2+ TB RAM machine, but thread count is limited with -j128.
Baseline, Compiled with GCC 11.4, PCH disabled
Baseline, Compiled with Clang 23git, PCH disabled, ClangBuildAnalyzer
Baseline, Compiled with GCC 11.4, PCH enabled
Baseline, Compiled with Clang 23git, PCH enabled, ClangBuildAnalyzer
Baseline, SPEC CPU 2017 Fortran build time[3]
Key takeaways (maybe you find more interesting details):
- Clang (132.6s) compiles a bit faster than gcc (164.7s) according to the wall clock, but consumed more user CPU time. This is due to the longest TU taking more time with GCC (160.2s vs 130.5s). That is, due to the massive parallelism available, compilation is dominated by the slowest TU build.
- GCC requires a lot more memory (up to 8 GB RSS per TU) than Clang (4.2 GB)
- lib/Semantics/check-omp-atomic.cpp is the most expensive TU. With Clang, lib/Lower/OpenMP/OpenMP.cpp requires the most memory.
- With PCH enabled, gcc wall time is slower (due to longer critical path) and requires more (!) memory, but user time is 9% less user time
- With PCH enabled, clang’s wall time is 5% slower, but uses 1/3rd less memory and 27% less user time. Compare to [flang] Use precompiled headers in Frontend, Lower, Parser, Semantics and Evaluate by mrkajetanp · Pull Request #131137 · llvm/llvm-project · GitHub
- Template instantiation takes 37% of the CPU time (Clang, using patch adapted from ⚙ D36946 [time-report] Add Template Instantiation Timer). GCC’s -ftime-trace reports even 45%.
- As expected, std::visit and std::variant by far dominate the template costs. Flang has its own visit implementation that claims to have less overhead, but seems to used a lot less often.
- Flang performance is improved by
-mllvm --mlir-disable-threading. On this system (512 logicial threads), MLIR multithreading increased wall time by 36%, User CPU time by 165%, and System CPU time by 30000%.
Lowest-Hanging Fruit: KIND template parameter
Removing template parameters has already been done on Flang-RT ([flang-rt] Rework findloc.cpp to dispatch target at runtime by jhuber6 · Pull Request #197756 · llvm/llvm-project · GitHub, [flang-rt] Rework findloc.cpp to dispatch target at runtime by jhuber6 · Pull Request #197756 · llvm/llvm-project · GitHub), including removing KIND and converting template parameters to function arguments or class members. Removing KIND would reduce the number of Expr/Type-related template instantiations from 20 to 7.
The code is available here: [Flang] KIND De-Templatization by Meinersbur · Pull Request #206907 · llvm/llvm-project · GitHub. This was mostly done by AI and not intended to land in this form. Despite being based on a 3 weeks old commit and touching most files, it does not have a merge conflict with main yet.
KIND template removal, Compiled with GCC 11.4, PCH disabled
KIND template removal, Compiled with Clang 23git, PCH disabled, ClangBuildAnalyzer
KIND template removal, Compiled with GCC 11.4, PCH enabled
KIND template removal, Compiled with Clang 23git, PCH enabled, ClangBuildAnalyzer
KIND template removal, SPEC CPU 2017 Fortran build time[3:1]
Key takeaways
- GCC reduced to 89.5s (-46%) Wall time, 4938s (-31%) User CPU time, Up to 5.1 GB (-37%) MaxRSS per TU
- Clang reduced to 86s (-35%) Wall time, 7692.9s (-35%) User CPU time, Up to 3.8 GB (-21%) MaxRSS per TU
- 37% fewer std::visit template (
std::__do_visit) instantiations - The most costly TUs now are lib/Parser/openmp-parsers.cpp, lib/Lower/OpenMP/OpenMP.cpp, lib/Semantics/check-omp-structure.cpp
- Fortran build time changes are below noise level.
Conclusion/Opinion
IMHO de-templatization is worth the one-time churn of applying such a patch. The KIND template parameter was just the lowest-hanging fruit and already showed quite significant build time/memory improvements. I also think Flang should not need to maintain its own arbitrary-precision implementation for ints and floats when LLVM already has one. I generally find Flang’s style of auxiliary classes for sum types (e.g. InternalSubprogramPart) and product types (e.g. DeclarationConstruct) with meaningless member names such as u and t are hard to read and pose trouble with IDE support. The long build times make a edit-compile-debug cycle a churn. Long template error messages makes me rely on AI to fix it.
I would go even further an update Flang’s object model to follow closer what LLVM/Clang/MLIR is doing and push the compile memory usage below the 2GB limit:
- Proper class hierarchies
- Custom RTTI type ID that describe the underlying Parse/Evaluate tree node type like LLVM/Clang. Allows the use of
llvm::cast<>instead ofstd::get<> - Interfaces either like LLVM faced (e.g.
MemIntrinsic) or MLIR-style concepts[4] - Dispatches like std::visit should be only present in .cpp files. HMHO it’s a sign that an abstraction is missing.
- Use of flang-tblgen to generate some of the class boilerplate instead of templates/preprocessor macros.
- De-templatization allows moving more code into .cpp to be processed only once, instead of included into many TUs and object files.
