Skip to content

Give the memory of freed large buffers back to the system with glibc - #434

Open
Tutez64 wants to merge 1 commit into
openfl:mainfrom
Tutez64:perf/trim-freed-large-memory
Open

Tutez64 wants to merge 1 commit into
openfl:mainfrom
Tutez64:perf/trim-freed-large-memory

Conversation

@Tutez64

@Tutez64 Tutez64 commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

glibc keeps a freed buffer's memory in its heap, unless it had mapped the buffer on its own. It maps
buffers of 128 KB or more on their own at first, but each time one is freed it raises that size to
the freed buffer's, up to 32 MB on 64 bits, so later buffers of those sizes come from the heap and
stay there once freed. After a burst of large allocations the process stayed as large as the burst,
although the collector had freed the buffers and MEM_INFO_RESERVED no longer counted them.

In an OpenFL game that reads 17 MB texture files while a level loads, mallinfo2 showed 722 MB of
free memory held in glibc's heap once the level was loaded, out of 1740 MB of anonymous memory, and
a malloc_trim(0) at that point brought it down to 1117 MB. Setting M_MMAP_THRESHOLD alone
(128 KB) left 263 MB free in the heap, from the buffers under that size.

The collector now counts what it frees to the C heap, the large buffers and the block groups, and
calls malloc_trim once that reaches 16 MB. The trim runs on the thread that frees dropped buffers,
after its batch, so no other thread waits for it. A collection that frees its buffers itself, as a
compacting one does, trims before it returns, and a collection with no buffers to drop still wakes
the thread when a trim is due, for what the sweep freed. Nothing changes without glibc.

In the game, over a two minute run through a town and a level: 23 trims of 0.4 to 26 ms each (the
longest for 360 MB), resident memory in the level from 2106 MB to 1574 MB, and no frame over 9 ms
with or without the change.

Tests: test/regression/LargeFreedMemoryReturned drops 384 MB of 4 MB buffers, after freeing some
8 MB ones so that glibc takes them from its heap. It stayed at 396 MB resident and now returns to
12 MB. Where /proc/self/statm is missing it passes without measuring. The other Large*, Gc*
and Generational* cases, ReleaseBlockSizedLarge, MemInfoLiveBytes and
ManagedMemoryOverRelease pass, 47 in all (Linux, g++ 14, -D HXCPP_M64).

glibc keeps a freed buffer's memory in its heap, unless it had mapped
the buffer on its own. It maps buffers of 128 KB or more on their own at
first, but each time one is freed it raises that size to the freed
buffer's, up to 32 MB on 64 bits, so later buffers of those sizes come
from the heap and stay there once freed. After a burst of large
allocations the process stayed as large as the burst, although the
collector had freed the buffers and MEM_INFO_RESERVED no longer counted
them.

In an OpenFL game that reads 17 MB texture files while a level loads,
glibc held 722 MB of free memory once the level was loaded, out of
1740 MB of anonymous memory. With this change the same point is at
1160 MB.

The collector now counts what it frees to the C heap, the large buffers
and the block groups, and calls malloc_trim once that reaches 16 MB.
The trim runs on the thread that frees dropped buffers, after its batch,
so no other thread waits for it: it took up to 26 ms in the game, for
360 MB. A collection that frees its buffers itself, as a compacting one
does, trims before it returns. A collection with no buffers to drop
still wakes the thread when a trim is due, for what the sweep freed.

Tests: test/regression/LargeFreedMemoryReturned drops 384 MB of 4 MB
buffers, after freeing some 8 MB ones so that glibc takes them from its
heap. It stayed at 396 MB resident and now returns to 12 MB. Where
/proc/self/statm is missing it passes without measuring. The other
Large*, Gc* and Generational* cases, ReleaseBlockSizedLarge,
MemInfoLiveBytes and ManagedMemoryOverRelease pass, 47 in all.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
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.

2 participants