Summary

We propose an opt-in Clang feature, -fenable-readonly-thp, that implements iTHP (instruction Transparent Huge Pages) at compile and link time.

The driver arranges 2 MiB (or larger) segment alignment at link time, links a small startup object, and at process start calls madvise(MADV_COLLAPSE) on the executable text range so the kernel can promote default 4 KiB page mappings to huge pages.

A prototype patch is available; we welcome feedback on this RFC before posting the patch series to Phabricator.

Motivation

Large binaries with big instruction footprints can be frontend-bound due to iTLB pressure from 4 KiB text mappings. Linux can promote eligible executable .text regions to 2 MiB pages when alignment requirements are met and the process opts in via madvise().

Today this requires manual linker scripts and custom startup code. A compiler-managed, opt-in path makes the optimization practical for production builds.

We measured performance uplift with SPEC CPU 2026, Geekbench, and other benchmarks (CPython, database/server workloads, micro-architectural tests). Gains are workload-dependent—strongest on large .text workloads such as interpreters, simulators, and compiler-like binaries; some workloads are unchanged. We did not see broad regressions in our testing.

Kernel requirements

Kernel version iTHP support
Linux 6.1+ (minimum) iTHP works on tmpfs-backed executables only (MADV_COLLAPSE, shmem THP)
Linux 7.2+ iTHP works on both tmpfs and non-tmpfs (disk-backed) executables

Required system settings for iTHP:

  • /sys/kernel/mm/transparent_hugepage/enabled set to madvise or always
  • CONFIG_READ_ONLY_THP_FOR_FS=y in the kernel (required for non-tmpfs / file-backed THP on Linux 7.2+)

Below Linux 6.1, or when THP is disabled, behavior falls back to normal 4 KiB mappings.

Runtime behavior

With MADV_COLLAPSE, the kernel is asked to collapse eligible 4 KiB pages into huge pages immediately when madvise() is invoked on an aligned range. This differs from hint-based approaches (e.g. MADV_HUGEPAGE) where the kernel may defer promotion to a page fault or khugepaged. Promotion is not guaranteed if kernel preconditions are not met.

When the feature is active on a supported kernel, the resulting THP mappings can be observed in /proc/<pid>/smaps:

smaps field Mapping type
ShmemPmdMapped shmem / tmpfs-backed executables (Linux 6.1+)
FilePmdMapped non-tmpfs / disk-backed executables (Linux 7.2+)

Proposed interface

This proposal introduces two new Clang driver options:

-fenable-readonly-thp              # default 2 MiB segment alignment
-fenable-readonly-thp=<size>     # 2M, 4M, 8M, 16M, 32M, 64M, 128M, 256M

Target: x86-64 Linux ELF (initial scope)
Linkers: LLD or GNU BFD ld

Comparison with BOLT hugify

Both approaches aim to improve iTLB performance, but differ in scope, timing, and mechanism.

-fenable-readonly-thp BOLT -hugify
When Compile/link time Post-link (BOLT is a post-link optimizer)
Profile Not required Profile-driven: instrument binary, collect profile, re-link with -hugify
Scope Entire executable .text segment Hot code only, selected from profile
madvise flag MADV_COLLAPSE — requests immediate collapse at call time on eligible ranges MADV_HUGEPAGE — hint only; promotion not guaranteed or immediate

These two are complementary: BOLT targets profile-identified hot regions after link; this proposal provides a general compile-time path for whole-text promotion without instrumentation or a post-link pass.

Trade-offs

  • Binary size: Padding to alignment boundaries increases .text size; the feature remains opt-in.
  • Startup cost: A one-time madvise() during process startup; overhead is typically small.
  • Kernel dependency: Requires Linux 6.1+ (tmpfs only); non-tmpfs support requires Linux 7.2+.
  • Scope: Initial support is x86-64 Linux ELF with LLD or GNU BFD ld; extension to other architectures, object file formats, and linkers may follow.
1 Like

It’s unclear to me why this needs be a flag in the clang driver/a library maintained inside of clang.

At Google we have a library that does pretty much this (although with some more classification around what it should huge page, as we e.g., don’t want to keep all cold code resident if it’s not actually going to be called). But it’s a completely separate library that gets called on startup.

If one really wants to put this into a system library, I think it makes much more sense to do this in a place like libc where it should be possible to just do this as part of the normal process between _start and main. You need to manually ensure that the binary segments containing the sections you care about huge paging are 2MB aligned, but that’s a relatively simple linker flag. I’ve been looking into trying to get this supported in LLVM libc, but haven’t gotten around to it yet.

1 Like

Thanks for your comment. The core idea is the same as yours — the packaging is different. We propose a small startup object linked automatically by the Clang driver under -fenable-readonly-thp (opt-in, not default), while you use a separate startup library. The other difference is that we target the entire .text range (between linker-defined boundaries) rather than only hot regions. That fits the common case where large binaries benefit broadly from lower iTLB pressure, avoids profiling or runtime classification, and keeps the compile/link-time work simple.

Concretely, our approach involves the Clang driver emitting the linker page-size alignment flags, injecting a linker script that marks the load boundaries, and linking the startup object from the Clang resource directory. At runtime, a constructor in that object reads those boundaries and calls madvise(MADV_COLLAPSE) on the range.

Regarding LLVM libc vs. the driver: libc _start → main is a natural long-term place for this, but most Linux programs still use glibc, so a driver option makes the feature usable today without migrating to LLVM libc.

Have you proposed this for inclusion in the glibc? What is the opinion of the glibc maintainers on this? (cc @fweimer-rh)

I haven’t seen a concrete discussion of a suitable mechanism. Increasinig p_align probably isn’t it because the increased alignment means that code may no longer be in the critical window for some indirect branch predictors. And we cannot disregard p_align because it may not be just an optimization request.

Right. I understand what your proposal does. It’s not clear to me why this needs to be in the driver though. Why can you not just link in an additional library that does this that’s not maintained inside of clang and manually set the alignment flags at link time. You get the same result without needing to modify the compiler.

I’m not sure this is a general enough of a solution to put into a system library/compiler driver. We can have some sensible defaults, but I’m not sure that huge paging the entire RX segment is that. I would expect that anyone interested in enabling transparent huge pages has first done PGO (likely a much bigger win). Huge paging entire binaries, especially big server binaries that have a lot of cold code, increases RSS usage quite significantly, which is not ideal. You also have interactions with the medium/large code model. .lrodata for example is typically large and typically very cold. Using 4kb pages for that section probably makes a lot of sense. There are even more interactions once you enable static data partitioning where you end up with .unlikely sections for all of the data sections, which also make sense to 4kb page.

The additional complexity that falls out from better section classification is also pretty trivial. You do need to parse sections rather than just have dl_iterate_phdr return segments to you, but beyond that it just involves some dumb string matching to figure out what sections should be huge paged and what sections shouldn’t be.

FYI, for my use case (build most optimized clang as fast as possible on 8C16T with 128GB RAM), this feature is very useful. I obviously don’t care about RSS usage.

I use IRPGO as well. But static data partition sound like need memprof. I haven’t looked into exactly how it works yet, but I suspect it’s different from simple IRPGO.

1 Like

You can enable static data partitioning with just a sample PGO/IRPGO profile. Static data gets annotated with hotness levels based on the BFI of blocks that access that data. You can supplement the PGO data with a data access profile through MemProf and that probably gives additional performance, although I don’t have specific numbers.

A bit off topic, but have you tried this/collected performance numbers? If there are significant wins to be had, I’d be interested in trying to deploy this for LLVM premerge.

I haven’t measured the performance gain, but according to this article, the improvement is about 5%.

I first learned about this from this document. I tried to build clang with a IRPGO profdata (no memprof) and -fpartition-static-data-sections -Wl,-z,keep-data-section-prefix, but I didn’t notice any difference. Maybe I did something wrong?

Since nobody mentioned it, did you know this feature is already standard in GLIBC 2.44? It is enabled by default for Loongarch (and likely AArch64 once I’ve benchmarked it):

A new tunable, glibc.elf.thp, is added to map read-only segments with
  Transparent Huge Pages (THP) if THP is not disabled in the kernel.

It automatically aligns existing binaries without linker changes and gives significant gains.

I think this proposal might result in another madvise after GLIBC has done it. Is there an advantage to using MADV_COLLAPSE?

Note that large RW data and stack might also benefit from THP. This also seems better suited for the actual loader rather than as custom startup code.

1 Like

Thanks for the RFC. Regarding the “hugify” feature in BOLT, we have recently been experimenting with enabling huge pages for all text segments directly within the BOLT runtime (currently using a flag named --hugify-all-text, Draft PR).

The baseline implementation relies on madvise for file-backed huge pages, which has higer kernel version and configuration requirements (such as CONFIG_READ_ONLY_THP_FOR_FS). To address this, we tried an approach that remaps all file-backed memory to anonymous memory before applying huge pages. Testing shows that this method offers broader applicability and lower deployment overhead, delivering an extra 1%~2% performance gain over the baseline --hugify.

Since BOLT is regarded as a general‑purpose tool for optimizing front‑end bottlenecks, I am wondering if you think this would be a good idea?

Thanks for the GLibC pointer — loader-side THP for load segments is the right approach. From experimenting with glibc.elf.thp, two possible improvements: relax the ≥2 MiB segment threshold so smaller apps benefit, and consider MADV_COLLAPSE instead of MADV_HUGEPAGE for immediate, more predictable collapse on mapped text — MADV_COLLAPSE also avoids the CONFIG_READ_ONLY_THP_FOR_FS dependency that read-only MADV_HUGEPAGE may require on some kernel versions.

Is there any plan to enable this by default on x86-64 as well, similar to LoongArch (and possibly AArch64)?

Linux kernel versions for MADV_COLLAPSE (if GLibC adopted it):
tmpfs/shmem — Linux 6.1+;
non-tmpfs file-backed — Linux 7.2+. From 7.2, the CONFIG_READ_ONLY_THP_FOR_FS requirement is also relaxed.

1 Like

It’s odd that this would have increased performance. Moving to anonymous memory also has its drawbacks. File backed huge pages can be shared across processes. This is useful in a lot of setups like running a compiler where you might have multiple instances of the binary running at the same time, or when you’re running multiple instances of a server on a single machine. Anonymous memory also pins all memory as resident which further increases memory usage compared to demand paging. At the least binaries I look at typically have a lot of data in PT_LOAD segments that is extremely rarely/never accessed (even for whole 2MB chunks), which is nice to never need to load in.

[MemProf][Docs] Add more information on static data partitioning by boomanaiden154 · Pull Request #220446 · llvm/llvm-project · GitHub should improve the documentation (I presume you didn’t setup a linker script), although we want to add an additional lld flag to avoid the need for a linker script.

1 Like

Thanks for the feedback! The performance gains likely come from two main reasons:

  1. Low success rate: Due to the kernel requirements, the baseline file-backed huge pages actually failed to take effect on most of the machines in the environment when comparing at cluster level.

  2. Coverage for DSOs: Huge pages are applied to the text segments of DSOs(perhaps, at least for the main binary, we could consider restricting the anonymous remap to only the hot regions in the future).

In our internal environment, a single physical machine typically runs a number of virtualized containers for different services (which I assume is also the norm for most data centers). Therefore, cross-process sharing of file-backed memory seems rarely utilized in the first place. BTW, I’ve noticed that other languages like Java and Go often rely on a similar anonymous memory approach in practice to enable THP for their code segments.