I compiled Unreal Engine 5.7.4 with HMake: implementation, benchmarks, and reproduction steps #4
HassanSajjad-302
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
benchmarks.txt
Hello everyone,
Some time ago, I posted about my software, HMake. HMake supports pioneering IPC-based compilation, which I used to compile Clang 2x faster using C++20 header-units. I proposed it as a potential CMake replacement for LLVM in this LLVM discussion, but it was not accepted because it lacked organizational backing.
I also asked the Unreal Engine community to review HMake as a UBT replacement. The feedback was to demonstrate it with Unreal Engine first.
After 1.5 months of effort, with help from Sol-Ultra and now Astra, I have compiled UE5 with HMake. I chose UnrealServer-Linux-Debug because it is simpler and faster to compile than the other configurations.
hmake-5.7.4branchFeatures added for UE5
1. Distributed build specifications
HMake uses a single-file build specification, while UBT uses multiple files. I added
scanner.py, which finds.hmake.hppfiles throughout the child directories and generates anhmake.cppthat includes them.Each file defines a
specifyfunction containing its build configuration. This gives HMake a UBT-like build-specification workflow.HMake's
ue.hppandue.cppextend its existing types with UE5-like APIs. For example,DSC<CppTarget>::depstakes dependency pointers, whereas UBT specifies dependencies using string literals. The UE integration finds the specification associated with a dependency name, configures it, adds the dependency, and returns control.This is managed by
UeConfiguration, derived fromConfiguration, andUeCppTarget, derived fromCppTarget.2. Adaptive jumbo-build support
HMake now supports adaptive jumbo builds, like UBT.
3. ISPC compilation support
HMake now supports ISPC compilation.
4. Cyclic target dependencies
HMake now supports cyclic
CppTargetdependencies.Other improvements
5. Single-command builds
Building a project with HMake is now a one-step process using
hbuild.CMake builds HMake itself. Its script defines toolchain macros when compiling HMake's libraries. HMake can then use the same toolchain to compile both your project's
hmake.cppand the project itself. The API provides several strategic points for overriding toolchains or compilers.For UE5, running
scanner.pygenerates the HMake specification. Users do not need to add or configure a toolchain: UE compilation uses the exported commands for UE's bundled tools. HMake automatically uses its existing default host toolchain for the generatedconfigureandbuildexecutables.6. Early cutoff, including dynamic dependencies
HMake is very advanced in this regard.
Suppose a custom code generator produces output identical to the existing file on disk. In its override of
BTarget::isEventCompleted, you simply changeupdateStatusback toUpdateStatus::UPDATE_NOT_NEEDED.Consider a header-unit
Awith a static dependency ongenerated.h, and a header-unitBwith a dynamic dependency onA.Because of its static dependency,
Acan only build aftergenerated.his generated. However,Bcan already launch its compiler process: its build cache says it depends onA, and, at that point,Aneeds an update even though its process has not launched.Now suppose
generated.his unchanged. ItsBTarget::isEventCompletedreverts its update status. WhenAis scheduled, it notices that the target recorded as its reason for rebuilding now hasUPDATE_NOT_NEEDED. It callsBTarget::setUpdateStatusagain to check whether another dependency or change still requires rebuilding. If not, it returns without launching a process.Meanwhile,
B's running compiler discovers its dependency onA. Before responding, HMake checks whether the dependency that causedBto need an update has reverted its status. If so, HMake re-evaluates whether anything else still requires rebuildingB. If something does, compilation continues. Otherwise, HMake kills the launched process.Despite this advanced behavior, supporting it in a custom code generator remains easy.
Here is what Sol-Ultra says about early cutoff:
I only have a Plus subscription, which now has a 5-hour limit. Otherwise, I would have asked Astra-Ultra for a comment as well.
7. BMI compatibility
The 2-3 UE5 targets that use RTTI and exceptions are compiled in a separate configuration. The consumer configuration uses only their
.ofiles.Each configuration can therefore compile with its own BMIs without compatibility problems. Header-unit BMIs compile extremely quickly, and only those are duplicated.
I used Sol-Ultra extensively and now work 2x-3x faster with its help. I reviewed every single line that went into HMake.
Header-units and the next step
I have not added precompiled-header support to HMake because header-units make it redundant.
UE5 currently includes >10k header files, but only a subset is consumed through PCHs. With HMake, every one of these could be consumed as a header-unit. That would be too granular, however, so HMake also supports big header-units, which would be the default for UE5.
Instead of 10000 individual header-units, UE5 would compile with 400 big header-units: one per target, averaging 25 header includes per header-unit.
My testing with some jumbo
.cppfiles compiled using header-units has shown very positive results. I can assure you that we would get a 2.5x-3x build speedup for Debug and 2x-3x for Release.So, what is left?
Header lookup
In traditional compilation, a header is included by its include name. The compiler searches directories in order and stops when it finds a matching file. Changing the include-directory order can change which header is found, and this search involves numerous filesystem calls.
With header-units, when HMake receives an include name, it looks up the corresponding
CppModin one central hash map and shares it with the compiler. It also shares the dependencies, keeping the number of exchanged messages small.The current blocker is developing this hash map. In LLVM, for example, header names follow a pattern such as
llvm/target-name/header-name. Including bothllvmand the target name keeps each include name unique.For UE5, we need to change include names throughout the source to make them unique. These names only need to incorporate one or two parent-directory names. I think this can be achieved very easily with scripts.
UE 5.7.4 also uses an older Clang 20. Given UE5's sheer size, I expect some compilation errors and possibly compiler crashes when using header-units. These should be easy to fix, and I would still categorize the transformation as drop-in.
Remaining work
I have not implemented UHT support or the other custom tasks required by additional configurations. A lot remains to be done, including support for architectures and platforms such as Android, iOS, and PlayStation. Currently, no architecture other than x86 is supported.
I believe the remaining work can be completed very quickly with help from LLMs.
Benchmarks
I have attached a file showing the
hbuildbenchmarks.Currently,
hbuildis 1.6x slower because it uses only jumbo builds, whereas UBT uses both jumbo builds and PCHs. However,hbuildis 4x faster on zero-target builds. On my 14900hx system, a zero-target build takes around 400ms.There are still multiple improvements to make. The benchmarks already show excellent HMake performance characteristics, such as CPU usage of 23.3% with a 24-thread allocation.
Steps to reproduce
These steps target Linux x64 and UE 5.7.4, UnrealServer-Linux-Debug.
Prerequisites
<format>and<print>.The commands assume fresh sibling checkouts under
~/Projects.1. Build HMake and add its build directory to PATH
Keep both the HMake source checkout and its CMake build directory:
hbuilduses the associated headers and libraries. ThePATHchange applies to the current terminal. Add it to your shell configuration if you want it to persist.2. Prepare and build UE with UBT
Use the
hmake-5.7.4branch of my fork, not its newerreleasebranch.HMake currently consumes UBT-generated files, so this reproduction sequence starts with the full UBT build.
3. Export the metadata for HMake
The ordinary
makecommand above does not write the JSON metadata consumed by the scanner. Generate it separately:This writes:
Leave UBT's PCH and shared-PCH settings enabled. Continue only after each command succeeds.
4. Copy the scanner and regenerate hmake.cpp
From the UnrealEngine root:
cp ../HMake/Projects/UE5/scanner.py . chmod +x scanner.py ./scanner.pyNo scanner arguments or custom toolchain configuration are needed. The scanner generates
hmake.cppandEngine/Source/HMakeSharedDefs.h.5. Build with HMake
mkdir -p uebuild cd uebuild hbuild -j 24The expected executable is at the following path, relative to the UnrealEngine root:
Run
hbuild -j 24again fromuebuildfor an incremental or zero-target build.Feedback
HMake's front end is simple, but its implementation contains logically complex behavior, so a complete review would require some effort. Before diving into the code, you could ask Astra-Ultra or Fable-5.1 to assess HMake and my claims. Please share any mistakes you find.
I believe that, at this point, with the cost of adoption so low thanks to frontier LLMs, not switching to HMake would be an irrational decision.
All reactions