here is a summary of changes related to memory allocation that I have
been working on recently. Included are dynamic space relocation,
incremental allocation of gencgc pages, read-only space allocation
without os_validate, and a "soft" heap size limit.
Please find the patch at:
I will probably not have much time for further testing or development of
these changes, so I am hoping that someone else takes an interest in
Some testing on GENCGC platforms other than Linux/AMD64 and Linux/x86
would be required before this patch could be considered finished. (The
newer and somewhat experimental parts are conditional on
#+incremental-allocation though, while the older relocation stuff, which
is compiled unconditionally, has worked well for a quite some time now.)
* Relocation can be tested using tools-for-build/librelocate.c, which
compiles to a .so file that can be preloaded to trigger relocation.
See the instructions in that file for details.
* For incremental allocation, there is a test in core.test.sh to see
if core file saving works with a hole in dynamic space. It's a
small test, but already covers tricky code paths.
* For the soft heap size limit, see tests/soft-limit.test.sh.
* If you want to try incremental allocation, be aware that parms.lisp
needs to set a DEFAULT-DYNAMIC-SPACE-START low enough to avoid the
need for relocation in warm init (apparently the cold core is
currently not relocatable, not sure why). For AMD64 and Linux/x86 I
have already set a suitable DEFAULT-DYNAMIC-SPACE-START. Other
GENCGC platforms still need to be changed and tested in the same
(How can you find a good value? The theoretical answer is to take
the end of linkage table space, and round it up "a little". The
more practical solution goes like this: Try to compile without
fixing parms.lisp and watch it hang in warm init. Look for the line
that says "relocating dynamic space to: 0x12345678". Round up that
0x12345678 to the next 64k boundary and use the result as
Dynamic space relocation
This is basically the same patch as one year ago, but forward-ported to
relocate.c implements routines that run over parts of the memory and
rewrite all addresses in them to adjust for their movement into a
different part of memory.
On startup, we try to get dynamic space at the default position. If we
cannot get that position, we relocate it to an arbitrary new position
handed to us by mmap.
The current version has been tested on Linux/AMD64 only. Last year, I
had done more extensive testing with the following results, which are
probably still valid:
- works on Linux, FreeBSD, and Windows
- would work on Solaris, but its mmap() doesn't take address hints
into account without MAP_FIXED, so we can't relocate only if
needed. Instead we use MAP_FIXED and don't relocate at all.
But new: With the incremental allocation (and a 64 bit system),
Solaris should work like the other platforms, since we get our
dynamic space from brk() then.
- other ports untested
Relocation works for both gencgc and cheneygc.
Incremental allocation allows SBCL to start with minimal virtual memory
use for dynamic space, and a page table that is as small as possible.
More memory is allocated only as needed. It has the following
- Thanks to a self-tuning page table size, users do not have to
specify a small absolute dynamic-space-size limit if they want to
avoid wasting too much memory on the page table.
- It avoids the need for a contiguous dynamic space, allowing a
relatively large dynamic space to be used on on 32 bit architectures
(without the risk of running into shared libraries).
- An SBCL with incremental allocation is compatible with kernels that
have overcommit disabled (without forcing users to pre-determine the
dynamic-space-size for each process in advance).
- Fewer confused questions by users who don't understand the VSZ
column in top. :-)
This patch is conditional on LISP_FEATURE_INCREMENTAL_ALLOCATION, and
almost entirely covered by #ifdef. (Minor exceptions where I just
changed the code unconditionally: gc_init() now happens a little later,
just before loading dynamic space. output_space() has been extended to
write several segments of memory at the same time. And a small number
of tests for page table flags have been rewritten.)
My implementation of incremental allocation works as described below.
(It is not related to the older patch for CMUCL by Peter Van Eynde.)
- available for gencgc only
(On 32 bit systems, needs an mmap that respects address hints
without MAP_FIXED -- i.e. not Solaris.)
- allocates the memory for dynamic space using sbrk() instead of
mmap(), so that it sits as low in memory as possible. This way we
only have to extend the page table at the end, not at the beginning,
which would entail hairy renumbering of existing pages.
(See below for details and possible alternatives.)
- the amount of memory allocated initially is just enough to cover
dynamic space as contained in the core file. Only when
gc_find_freeish_pages() cannot find find free pages, it calls a
new function gc_map_new_pages(), which allocates new memory and adds
it to the page table (enlarging it as needed).
- When new pages are not adjacent to the current last page, we mark
the pages that been skipped as holes, using HOLE_PAGE_FLAG as its
- Before saving the core file, holes are removed using the relocation
code, see compact_dynamic_space_segments() in save.c.
- Some details regarding the implementation of gc_map_new_pages:
On 32 bit architectures, we get our new memory pages using mmap()
without MAP_FIXED, so that we can get memory from nearly all parts
of the address space without overwriting shared libraries
On 64 bit architectures, mmap() could return pages too far away from
dynamic space for one page table to be able to span that difference.
Instead, we use sbrk() on those architectures.
(I chose sbrk() because it seemed easier at the time. It would be
possible to pick a suitable location for dynamic space and allocate
using mmap() with MAP_FIXED instead. Doing so would mean to give up
the guarantee that conflicts with other mapping in memory are
detected gracefully, but then the 64 address space is so huge that
collisions are unlikely.)
Since Windows has neither brk() nor mmap(), it will have to use a
third strategy. Its memory allocation functions are rather clever
though, so I am sure it can be done somehow.
No more READ-ONLY-SPACE-START in parms.lisp
The read-only space is now an array defined in src/runtime/globals.c.
Its position is determined by the linker and propagated from sbcl.nm
into Lisp, rather than determined manually and copied from parms.lisp
into headers as it was before.
Thanks to Juho Snellman for the idea.
I am hoping to do the same thing for static space, but that still needs
a solution for the assembler code, which uses the address of NIL as a
constant in various places. (Previously NIL was just a #define for a
constant value. In the new scheme of things, we take the address of a C
variable `static_space', then round it up to a megabyte boundary (!),
before adding the constant offset for NIL.)
Soft heap size limit
In addition to the existing HEAP-EXHAUSTED-ERROR signalled when the
process is already out of memory, there is now a soft limit for the
number of allocated pages. Since incremental allocation does not have
to limit the page table size, I have reused --dynamic-space-size to
specify the soft limit.
Outside of GC, SOFT-HEAP-EXHAUSTED-ERROR is signalled before allocation
if the soft limit would have been reached afterwards. In GC, allocation
initially ignores the soft limit, but signals the error after GC if it
is still being violated.
Like the existing hard limit, the soft limit is a user-specified value
with no technical relation to the amount of memory made available by the
operating system. Unlike the hard limit, it tends to actually reach the
debugger without crashing on the way ;-).
The limit will be ignored while handling the error, and takes effect
again once the stack is unwound. Restarts are made available to change
or disable the limit, allowing the failed allocation to be retried.