ADR-348: The Memory main Still Holds Is Freed, Unless an Optimization Leaves It to the OS

Status: Accepted Date: October 14, 2026 Answers: whether a program frees what it still holds when main ends, or leaves the memory to the operating system (#607) Related: ADR-297 D5, D6 (cleanups, the drain, cleanup-deadline); ADR-306 D14 (optimization words); ADR-343 D1, ADR-346 D6 (how main ends); ADR-218 (no unsafe in std); Part III 13.3, 17.1

1. Question

When main ends, every value it still holds dies: its Drop runs, its cleanup runs (Part I 6.4), and its memory is handed back to the allocator one block at a time. The operating system takes the whole memory of the process when it ends anyway, so the last step changes nothing anybody can see; it only costs instructions. Should a program skip it?

2. Decisions

D1 — Freeing is the default; skip-free-at-exit:on leaves the memory to the operating system.

By default everything main still holds is freed as it dies, as anywhere else in the program.

optimization = "skip-free-at-exit:on" in nikaia.toml, or --optimization=skip-free-at-exit:on for one build, changes only the memory: from the end of main’s last statement until the drain begins, every value still dies in order, its Drop and its cleanup still run, but the block it occupied is not handed back. The drain (unjoined tasks, parked cleanups, cleanup-deadline), the exit status and what a detached task’s error prints are those of the default. A main that leaves by return, by an error or by process::exit frees as by default.

What the option changes is written into the program: an allocator that is the system’s and skips a free in that window, and one call after main’s last statement. A program built without it carries neither and pays nothing.

D2 — No effect outside the process depends on the option.

The option skips frees, not deaths: nothing that writes, flushes, closes or rolls back is skipped, so no Drop in std has to become a cleanup first. The audit, on crates/nikaia-std/src at 0.0.918, for the record: no Drop in std has an effect outside the process. PollingMain, Armed, InFlight, Sender, Released, Own, Frame, Cancellable, InATask, Watch, OpenBlock and the Cleaned wrappers act on the runtime’s own state (a queue, a count, a registration, a lock), and Cleaned parks the value’s cleanup, which then runs in the drain. std holds no BufWriter or LineWriter; a File and a socket close their descriptor, which the kernel does at the end of the process in the same way.

3. Reasons

4. Rejected

5. Measured

benches/brc (1 000 000 rows, about 400 stations), callgrind, instructions of the whole process, release builds on the same commit:

user-parallelism = "no", remove-bounds-checks:on,remove-overflow-checks:on, opt-level = 3, no LTO; the same output byte for byte:

build instructions conditional branches mispredicted
default 300 192 748 40 946 704 1 606 404
skip-free-at-exit:on 300 184 756 40 947 386 1 574 539

The free at the end of brc costs about 8 000 instructions, 0.003 per cent: a map of about 400 stations. What the option saves grows with the number of blocks main still holds at its end, not with the work the program did.

This site is open source. Improve this page.