All Manuals > LispWorks® User Guide and Reference Manual > 11 Memory Management

11.5 The Mobile GC

The Mobile GC is a 64-bit GC that is written to run on 64-bit iOS (in the future it may be used on other platforms, for example 64-bit Android). When LispWorks is delivered for 64-bit iOS, the "saved image" (the code in the object file that delivery creates) switches automatically to use the Mobile GC. Thus you are always using the Mobile GC when running on 64-bit iOS, and you are not required to do anything about it.

The default parameters of the Mobile GC are intended to be useful for most applications and in many cases you do not need to do anything to tune the Mobile GC.

11.5.1 Mobile GC changes to common functions and macros

This section describes changes to the behavior of GC-related functions and macros when using the Mobile GC compared to the ordinary 64-bit GC. For most applications, room, and maybe gc-generation, are the only interesting functions. Specific functions for the Mobile GC are not discussed in this section.

mobile-gc-p

Returns true when running the Mobile GC.

room

The output of room is different for the Mobile GC. The last line (and the entire output of (room nil)) is the same, but the more detailed output is different. Without any argument, or if the argument is :default, LispWorks outputs the allocated and free sizes according to these types:

Cons
cons object only.
Other
All other objects, except static objects and large objects (> 1 MB).
Static
Static objects.
Large
Large objects (> 1 MB). Note: this threshold may change in the future, but it is fixed in the current version.

The Cons and Other segments are divided according to their generation and there may also some permanent segments (as a result of a call to make-current-allocation-permanent, make-object-permanent or make-permanent-simple-vector).

In addition, LispWorks also holds some reserved segments that are used during GC, and room prints the size of these too.

The output of (room t) also includes the segments for each type. For each segment, it prints the start and end addresses (in hex), the allocated area, and whether there is a free "hole" in the middle of it. For the Large and Static segments, it also prints the generation number of each segment. Permanent Static and Large segments have generation number 3.

See 11.5.2 Mobile GC technical details for more technical details.

gc-generation

When the gen-num argument is a number, it must be 0, 1 or 2. The value t (and :blocking-gen-num) is interpreted as 2.

Generation 0 is always promoted, but the :promote keyword affects generation 1 and, if non-nil, promotes even if promotion was blocked by set-promote-generation-1.

The keyword :coalesce is interpreted as in the ordinary 64-bit GC. The keyword :block is ignored.

marking-gc

Calls gc-generation with the gen-num argument. It is not useful in the Mobile GC.

clean-down

Performs the same GC as (gc-generation t).

reduce-memory

The full argument can be also be :aggressive, 0 1 or 2.

count-gen-num-allocation

The gen-num argument can be 0, 1, 2 or 3 (3 means permanent).

in-static-area

This does not affect allocation of conses (which are never static in the Mobile GC).

apply-with-allocation-in-gen-num

The gen-num argument must be 0, 1 or 2.

sweep-all-objects

Does not sweep cons objects in the Mobile GC.

sweep-gen-num-objects

Does not sweep cons objects in the Mobile GC.

The following functions do nothing in the Mobile GC, and the values that they return are not meaningful:

set-delay-promotion
 
set-maximum-segment-size
 
set-default-segment-size
 
set-gen-num-gc-threshold
 
set-blocking-gen-num
 
gen-num-segments-fragmentation-state
 
set-spare-keeping-policy

11.5.2 Mobile GC technical details

This section describes the Mobile GC in more detail. For most purposes, you do not need to understand the technical details of the Mobile GC, because it is used automatically and it should just work. You may want to know more if you want to fully understand the output of room (especially when called with t), and if you want to optimize memory usage (and maybe performance) of the application. In general, you should first use time or extended-time and room or room-values to understand the behavior of your application before trying to optimize it.

The ordinary 64-bit GC is "sparse", which means it leaves unused addresses between memory that it has allocated, and it also relies on being able to map memory at specific addresses. The result is a very efficient GC. However, on 64-bit iOS the range of addresses that is available (the address space) is very small compared to other 64-bit architectures (as determined experimentally because Apple do not documented it), and also there is no documented interface for mapping at specific addresses. Therefore the ordinary 64-bit GC cannot work on 64-bit iOS, which is the reason for introducing the Mobile GC. The Mobile GC is less efficient than the ordinary 64-bit GC, but the only interface that it requires from the underlying OS for memory handling is malloc and free.

An additional issue specific to iOS is that iOS does not allow execution of machine code that is created dynamically, and the memory region where the code resides is read-only. Therefore the Mobile GC does not support compilation of code in memory at run time. Moreover, functions can contain data that can be modified so this needs to be separated from the code, which is not the case in the ordinary 64-bit memory model. To support this, the images that are used to deliver on iOS are different from the desktop images, though the difference is only in the memory layout of function objects, and from the programmer's point of view they behave the same. These images differ from the ordinary 64-bit images in that function objects and code are separated, and that function objects are allocated in the same segments as symbols (that is the allocation type :symbol). The code is allocated in objects with allocation type :function. See 11.4.2 Segments and Allocation Types for more details about allocation types. The names of these images and how to use them are described in 17.1 Delivering for iOS.

The separation of code and use of the Mobile GC solves two different problems, which in principle could be solved separately. On 64-bit iOS, we have to solve both problems, and therefore the separation of code and the switch to the Mobile GC are done together.

11.5.2.1 Objects alive at delivery time

During delivery for 64-bit iOS, the code is separated out into its own block of memory (the "code block"). Then all of the other objects are put together in a block of memory, which is called the "data block". The data block is divided between non-pointer objects, weak objects and all other objects. The objects in the data block are never GCed, but the GC follows pointers from them to objects allocated at run time. Delivery creates an object file containing the code block and the data block, which is then linked with the rest of the app.

You cannot obtain a pointer to any object in the code block.

generation-number returns 3 for objects in the data block.

11.5.2.2 Objects allocated at run time

The Mobile GC has 4 different allocation types (note that these do not match the allocation types of the ordinary 64-bit GC described in 11.4.2 Segments and Allocation Types):

Cons
cons objects
Static
Static objects
Large
Very large objects
Other
All other objects.

The Mobile GC does not allow allocation of static conses. Weak objects are allocated as Other or Large.

The different allocation types are allocated in separate segments, where a segment is a contiguous block of memory. Each allocation type has a variable number of segments, which are printed by (room t). Each non-permanent segment belongs to a specific generation, which can be 0, 1 or 2. The permanent segments, which are created by make-current-allocation-permanent, have generation number 3, even though there is not really a generation 3 (the GC does not collect them).

Like in the ordinary GC, allocation of static objects makes life more difficult for the GC (so it reduces the efficiency of LispWorks), and should be avoided.

Objects that are larger than a threshold (currently 1 MB, but this may change) are allocated in segments with the Large allocation type and are also static.

The vast majority of allocation happens in segments with the Cons and Other allocation types, which are together called "ordinary allocation". The segments for ordinary allocation are all of size 8 MB, including any overhead. For Cons segments, the overhead is larger because conses do not have headers.

The Mobile GC mixes marking and copying techniques. Copying has the advantage of eliminating fragmentation and is also more efficient for typical applications where most allocation is very short lived. On the other hand, it requires spare memory to be available during the GC. Marking creates fragmentation and is slower when most of the objects are freed immediately, but it does not require extra memory. Thus the Mobile GC tries to use copying when possible (that is when it can get enough memory from the operating system), and otherwise uses marking GC. The two methods may be mixed in the same GC operation.

For copying, LispWorks uses reserved segments, which it obtains from the operating system as needed. At the end of the GC, it returns any segments that are no longer needed to the operating system, except for some segments that it keeps in reserve. The amount of reserved memory that it keeps is dynamic, and by default grows as the amount of allocation grows. By default, as long as the amount of memory in ordinary segments is less than 48 MB, LispWorks tries to keep enough reserved segments to copy everything in generation 0 and 1 without asking the operating system for more memory. see set-reserved-memory-policy for details.

The copying GC might promote objects, which means copying them to the next generation. Generation 0 objects that are copied are always promoted (that is copied to generation 1). For Generation 1 objects, it is more complex:

For automatic GC:

set-promote-generation-1 can be used to block any promotion from generation 1.

If promotion is not blocked (the default), then objects that have already survived a GC of generation 1 are promoted (copied to generation 2) and objects that are new to generation 1 remain in generation 1 (default setting) or are promoted depending on the setting by set-split-promotion.

For explicitly invoked GC by a call to gc-generation

The keywords :promote and :coalesce control whether objects from generation 1 are promoted or not.

Generation 2 objects are always copied into generation 2.

Blocking promotion from generation 1 can be used to prevent GCs of generation 2, as discussed in 11.5.3.2 Preventing/reducing GC of generation 2.

11.5.2.3 Special considerations for the Mobile GC

Because memory is more limited on mobile platforms, the Mobile GC is tuned to collect its highest generation (2) more often compared to the corresponding operation in the ordinary GC (which is a GC of generation 3). Such a GC may take enough time (in the order of a second) and be frequent enough to annoy users. If that happens then you need to try to tune your application, as described in 11.5.3.2 Preventing/reducing GC of generation 2, and you can also try to reduce the amount that your application allocates.

Very large objects (> 1 MB) that do not contain pointers are handled especially efficiently by the Mobile GC. For example, if your program handles a million small strings of 10-15 characters, then you can save memory and maybe even speed up your program by storing them all in a very large string, and use fixnums to specify the bounds of the small strings within the large string instead of using pointers to the small strings. This saves memory and makes the reduces the work that the GC needs to even if only half of the large string is actually used. Note also that when you finish with it, you can free a very large object and return its memory to the operating system without doing a GC by calling release-object-and-nullify.

When a very large object that may contain pointers (for example a large simple-vector) is examined by the GC, it needs to go through all of those pointers. This is wasted work unless either it is long-lived and is rarely seen by the GC, or it is almost full of useful pointers, or if you make it permanent. Objects in general can be made permanent by make-current-allocation-permanent, which is discussed in 11.5.3.2 Preventing/reducing GC of generation 2, but very large objects, which are allocated in their own segment, can also be made permanent individually by make-object-permanent or make-permanent-simple-vector. If most of the elements in a simple-vector are not pointers to objects that can be GCed, this substantially improves the performance of LispWorks.

Large objects which are allocated in their own segments can be explicitly freed (releasing the memory they use) by calling release-object-and-nullify. That releases the memory without a GC (so it is fast), and works on such objects even if they are permanent.

11.5.3 Tuning memory management in the Mobile GC

11.5.3.1 Response to low memory

Mobile platforms typically inform applications when memory availability becomes low. On Android this is done by the onTrimMemory or onLowMemory methods and on iOS by the didReceiveMemoryWarning method. It is probably a good idea to respond to these methods, but it is not essential.

In your implementation of these methods, you should release any system resources that can be released without loss and also try to reduce the memory used by Lisp data. Since the GC sometimes temporarily requires more memory during an operation, it may be a bad idea to do a GC once you get the warning. The function reduce-memory is provided to reduce memory usage without requiring more memory temporarily. Note that gc-generation can do a much better job than reduce-memory in general, but it may require more memory temporarily.

Calling reduce-memory with argument nil (the default) just releases any reserved memory that LispWorks has kept. It is fast and probably always a good idea. However, with argument nil, reduce-memory does not perform any GC operation, which in principle could release more memory. Because a GC takes time, it is not obvious whether it worth the trouble.

Calling reduce-memory with 0 or 1 causes a GC of generation 0 or 1, which is probably fast enough (unless promotion of generation 1 is blocked and generation 1 grows), but will not typically release much memory.

Calling reduce-memory with 2 (or, equivalently, t), or even :aggressive, can release much more memory, but takes more time, depending on the size of generation 2. Unless it is likely to release a large amount, it is probably not worth it. Thus, unless you know that generation 2 contains a lot of dead objects, you should only call reduce-memory with nil, or maybe 0 or 1.

If you call reduce-memory with a non-nil argument, you should first clear any caches that you have kept, so their contents can be GCed.

To be able to reduce memory usage, reduce-memory needs reserved memory to perform a copying GC. Since reduce-memory never obtains more memory from the operating system, its effectiveness depends on the amount of reserved memory that it has when it is called. Moreover, any call to reduce-memory frees all of the reserved memory (once the GC has occurred if the argument is non-nil), so calling reduce-memory with non-nil shortly after a previous call with nil is not going to be effective.

To see how much effect reduce-memory had on the memory, you can look at the output of room (last line with any argument you give it), or the result of room-values. To see how much time it takes, use the time macro or get-internal-real-time.

11.5.3.2 Preventing/reducing GC of generation 2

GC of generations 0 and 1 should normally be fast enough that you do not need to worry about them. GC of generation 2, however, typically takes enough time to be noticeable, and if generation 2 is large (> 100 MB) can take more than a second. Thus you normally want to avoid GC of generation 2.

In a "nicely behaved" application, which we believe is true for most applications, generation 2 never needs to be collected. This is based on the assumption that a nicely behaved application starts with some initialization that allocates long-lived objects, but then enters a "work" phase, where it allocates only short lived objects, which die before they reach generation 2.

Even if there is some "generation leak", that is objects being promoted from generation 1 to 2 that die not long afterwards, the leak may be slow enough that it is not a problem. For example, if your application "leaks" on average 1 kB each second, it would take close to 3 hours of operation to leak 10 MB, which is still too small to worry about (the default minimum size of generation 2 before a GC is 64 MB). So you can usually ignore this kind of leakage and hope that any occasional delay of a second or two after running the application for many hours is not too annoying for the user (though if it only a "generation leak" , you can do better by blocking promotion). If you have a leakage of 100 kB per second, the delay would happen every few minutes, which may be too annoying.

To find if your applications leaks to generation 2, you should periodically log the size of either the whole application or of generation 2. The output of room is the most useful thing to log, but you can also use room-values or count-gen-num-allocation. If the application does leak to generation 2, you should determine if it is a real memory leak, which means that the application accumulates live objects, or just a generation leak, which means that objects live long enough to reach generation 2 and then die. To determine that, call (gc-generation T) (or, equivalently, (clean-down)), continue using the application for a while and then call it again. If the leak is just a "generation leak", then the size of generation 2 after (gc-generation t) should stay (more or less) the same. If it grows, then you have a real memory leak.

If your application is "nicely behaved", generation 2, and hence the whole application, will initially grow, typically by few 10's of megabytes, and then will stay more or less fixed. The size of the whole application will always fluctuate, because generation 0 and 1 fluctuate, but generation 2 should be stable or grow slowly. If this is the case, you probably do not need to do anything further to control memory usage.

If generation 2 does grow, LispWorks will occasionally do a GC of generation 2, which takes a noticeable time (maybe a few seconds if generation 2 is few 100's of megabytes). If the leak is a real memory leak, it will also cause the application to grow indefinitely.

If the leak is a real memory leak, then the GC cannot do anything about it. One possibility is to make the application run for a limited time, for example by monitoring the size and quitting when it reaches some threshold. If quitting and restarting is possible without much loss, that may be a good solution. Most of applications probably want to avoid that though, in which case you will need to figure out what keeps objects alive and fix it. The functions sweep-all-objects, sweep-gen-num-objects and mobile-gc-sweep-objects can be used to check what kind of objects have accumulated. However, whatever keeps the objects is something in your application, and you will have to find it.

If the leak is only a "generation leak", then there are several ways to deal with it:


LispWorks® User Guide and Reference Manual - 01 Dec 2021 19:30:20