Skip to content

aotcompile: thunks for the entry points a pre-relocated image names - #63070

Open
levy wants to merge 4 commits into
JuliaLang:masterfrom
levy:pr-image-entry-thunks
Open

aotcompile: thunks for the entry points a pre-relocated image names#63070
levy wants to merge 4 commits into
JuliaLang:masterfrom
levy:pr-image-entry-thunks

Conversation

@levy

@levy levy commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Stacks on #63069.

A pre-relocated image cannot write the entry points of libjulia-internal into the file — jl_fptr_args and its companions, and the builtins — because that library moves at every start. On a 365 MB program those were 25,831 pointers over 8,500 pages, written at every start.

The image object now carries a table of thunks, one per entry point, each a musttail call through a slot that the runtime fills at start. A pre-relocation writes the thunk address instead, and a start fills 72 slots in one page. An image that is not pre-relocated keeps its direct pointers; the thunks are unused.

the same program, pre-relocated residual pointers start minor faults
without thunks 25,831 15.8 ms 11,111
with thunks 3 7.9 ms 2,824

Cost: one indirect jump per call through a runtime entry point, that is, per dynamic dispatch; not measurable on a 6.4 M-event simulation. The table has 128 entries so the image object has one shape; the runtime refuses if it needs more.

Part of #63065.

Disclosure: developed with Claude Code (Opus 5) under my direction. It wrote the code and this text; the measurements were run on my machine. I reviewed the changes. The commits carry an Assisted-by trailer.

levy added 4 commits September 8, 2026 18:40
A package image references them by tag, since they belong to the loading
runtime. The system image is that runtime, so it serializes them as objects,
and a field that points at one needs no relocation.

`jl_nothing` is already read from the image as a constant global. `julia_init`
allocates one before the root task exists, so the root task's fields that hold
that one are updated after the relocations, by a walk over the task layout.
Boxed integers need no adoption: two boxes of one value are equal by value.

A trimmed image keeps the tag encoding, to stay small.

Assisted-by: Claude Code (Opus 5)
A package image references a symbol by its index in a name list, and the
loader interns the list. The system image did the same, so every start
interned tens of thousands of names and wrote one pointer per reference.

The system image now serializes its symbols as objects, with their two tree
children, and records the root. The loader installs that tree as the symbol
table of the process after the relocations; a symbol interned later goes into
the same tree. `jl_init_common_symbols` runs after the restore, and
`jl_set_root_symbol` refuses a non-empty table. A trimmed image keeps the
name list, to stay small.

`julia --startup-file=no -e ''`: 66.8 → 61.4 ms, minimum of 15 pinned runs,
on top of the lowering-context change. `sys.so` grows 2.3 MB.

`test/cmdlineargs.jl` checks that a started process finds `nothing`, the
booleans, an image symbol, a freshly interned symbol and the root task's
`nothing` fields in the image, and that a new random symbol is not.

Assisted-by: Claude Code (Opus 5)
Applying the relocations of a system image writes almost every page of it:
47,566 page faults and 88 ms of a 105 ms `jl_init` on a 368 MB application
image. When the image is linked into a non-PIE program it is at the same
address at every start, so the relocation can be done once, at build time,
and its result written into the program file.

`--sysimage-prelink=yes`, when writing an image, reserves room for the
residual list: the pointers a file cannot hold. Without it the image is
unchanged. `--output-prelinked <file>`, on a program that holds such an image,
restores it, writes the program with the restored image to `<file>`, and
exits. Linux only, through /proc/self/exe.

A start of the written file applies the residual list, fills the gvar slots,
registers the native code and runs the fixup list; it reads neither
relocation list. The function pointers are resolved before the fixup list,
because the fixups write the first pointers a file cannot hold. The runtime
refuses an image pre-relocated for another address, one without native code,
one with several code variants, and a second pre-relocation.

A hello world starts in 7.1 ms instead of 74.9 ms, a network simulator in
7.9 ms instead of 101.8 ms (with the entry thunks; minimum of 15 pinned runs,
page cache evicted).

Adds the option to `--help` and NEWS, a devdoc section, option tests in
`test/cmdlineargs.jl`, and `test/prelink` (`make -C test prelink`, after
`test/embedding`): it builds a program with its own image, pre-relocates it,
runs both, and checks that the residual list stays under 64 pointers and that
the refusals fire.

Assisted-by: Claude Code (Opus 5)
A pre-relocated image cannot write the entry points of libjulia-internal into
the file (`jl_fptr_args` and its companions, the builtins): the library moves
at every start. On a 365 MB program that was 25,831 pointers over 8,500
pages, written at every start.

The image object now carries a table of thunks, one per entry point, each a
tail call through a slot that the runtime fills at start. A pre-relocation
writes the thunk address instead, and a start fills 72 slots in one page. An
image that is not pre-relocated keeps its direct pointers.

On the same program the residual list falls from 25,831 pointers to 3, the
start from 15.8 ms to 7.9 ms, and the page faults from 11,111 to 2,824.

Assisted-by: Claude Code (Opus 5)
@levy
levy force-pushed the pr-image-entry-thunks branch from 67d4e37 to c376d29 Compare September 8, 2026 16:42
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