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.
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.
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 object only.
All other objects, except static objects and large objects (> 1 MB).
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 Mobile GC technical details for more technical details.
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.
Calls gc-generation with the gen-num argument. It is not useful in the Mobile GC.
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
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 Segments and Allocation Types for more details about allocation types. The names of these images and how to use them are described in 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.
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.
generation-number returns 3 for objects in the data block.
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 Segments and Allocation Types):
Very large objects
All other objects.
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).
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:
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
Blocking promotion from generation 1 can be used to prevent GCs of generation 2, as discussed in Preventing/reducing GC of generation 2.
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 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 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.
Mobile platforms typically inform applications when memory availability becomes low. On Android this is done by the
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
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.
Once you have made this call, the automatic GC will never promote to generation 2 (explicit invocation of the GC ignores this setting). This is useful in situations where the "leaking" objects are live long enough to be promoted to generation 2, or the memory they use is small, so generation 1 does not grow too much. If there are many objects that live longer, then generation 1 will grow, and hence the GC of generation 1 will become slow. You should check if generation 1 grows, but it is probably OK if it remains at 20-30 megabytes allocated after a GC. You can try timing a GC of generation 1 by
(time (gc-generation 1)).
Note that you can switch promotion on and off as needed, so if you can identify phases in your application when allocation is not long-lived and phases when some is long-lived, then you can switch promotion on and off as appropriate.
Once you have called set-generation-2-gc-options with
:minimal-size-for-gc t, LispWorks will not automatically GC generation 2. It is then your responsibility to GC generation 2 at the appropriate time by calling
In an interactive application, you can have a "cleanup" option somewhere that invokes the GC, so the user can invoke it. You probably also want some indicator when the application has grown and needs a "cleanup".
For an interactive application, it may be a useful to do a GC when the application becomes backgrounded, but it is not obviously so. The method that is called by the operating system to indicate that the application has been backgrounded must return in a short time, so you probably need to invoke the GC from another thread. Also the operating system may not give much CPU to the application while it is in the background, and may even terminate background applications that take CPU. For example, the "App Programming Guide for iOS" says: "Apps that spend too much time executing in the background can be throttled back by the system or terminated." A GC of a 100-200 megabyte application should not take enough time to cause termination, but it depends both on the underlying system (hardware and OS) and the current state of it, so it is not that predictable. You certainly need to store anything that needs to be stored before doing a GC while in the background.
As long as memory is not constrained, the time it takes to GC generation 2 correlates to the amount alive after the GC rather than before the GC (because it uses copying, so does not touch dead objects). Therefore, if you have points in time in the execution when you know your application uses less memory then these are good points for doing a GC. That would be the case if your application builds a large data structure for a task (allocation of > 10 megabytes), and all this data becomes free when the task finishes. In this situation, it may be useful to perform a GC in the end of the task.
By calling set-generation-2-gc-options you can tune the frequency of GC of generation 2. You can either aim for infrequent GCs, which may be long but hopefully rare enough not to be too annoying, or aim for frequent GCs which are fast enough that they do not bother the user.
When the amount that is alive after a GC is almost always much less than the amount alive before, which is quite common, you can tell that to the GC by set-expected-allocation-in-generation-2-after-gc. This can significantly improve how well the GC copes when it fails to get as much memory as it asks for from the operating system. See set-expected-allocation-in-generation-2-after-gc for details.
The function make-current-allocation-permanent causes all the currently allocated objects to be made permanent, which means that the GC will not scan or free them in future (but it will still follow pointers from them). That is useful in the typical situation where the application starts with some initialization that creates long-lived objects. Using make-current-allocation-permanent at the end of the initialization makes all these objects permanent, and therefore reduces the time for GC of generation 2. If new objects in generation 2 after initialization are only the result of "generation leak" then the effect on time can be quite large.
LispWorks User Guide and Reference Manual - 20 Sep 2017