If you want to optimize your volatile memory usage, it is good to understand the basics of how the CircuitPython memory manager and garbage collector work. This section describes how the memory manager places newly created objects, and how and when the garbage collector cleans up old, unused objects. Once you understand these two memory workers, it is easier to understand the memory-saving tips in the next section.

To go straight to the tips, skip ahead to the next sections for a collection of RAM-saving techniques.

The volatile memory or "RAM" is used to store all the active variables that you use in your program.  Whenever you use a new variable, the memory manager allocates memory space for that object.  

Example: Creating a Bitmap in memory

When creating a Bitmap image of Blinka, the memory manager searches for the first available contiguous memory location that can fit the bitmap in its memory space. As shown in the animation, the memory manager scans from the beginning of the memory and looks for an available space in memory that can fit the Blinka bitmap. Once it finds a suitable-sized space, the memory manager stores the bitmap in that memory location.

Memory "references"

When objects are created (sometimes called "instanced") they are placed in memory and are "referenced" by other objects in memory.  By "referenced", this means that the object is somehow used by another object.  In the case of displaying a Bitmap on a display, the bitmap is placed into a displayio.TileGrid, which in turn is placed into a displayio.Group which is further shown on the screen by display.show().  In this case, the Bitmap is referenced by the TileGrid, since the Bitmap is one of the inputs when the TileGrid is created. These are all examples of references.

This concept of "reference" is important since it highlights when memory objects are still needed.  Whenever an object is still "referenced" by other objects, the object should be kept around.  However, whenever nothing references a given memory object, then that object is no longer needed.

Once a memory object is no longer referenced, it remains in memory as a “phantom” memory object. That memory space remains unavailable until it is identified as free by the garbage collector.

Example: "phantom" memory objects

Blinka starts playing a game using a Blinka bitmap icon (of course!), so the code creates a memory object with the Blinka graphic bitmap. Blinka finishes with a high score of 50, and that score is stored in memory just after the bitmap.  

After the game is over the graphics are no longer needed, so the bitmap is dereferenced (it turns to gray "phantom" in the animation). Now the game counts amount of time it was played (10 minutes, 55 seconds) and goes to store that value in memory.  

The memory manager doesn't recognize the dereferenced Blinka bitmap as free space.  

It goes straight past the "phantom" dereferenced bitmap and places the time object just after the score value. 

There is a solution: We need to call the garbage collector.

Two workers: Memory manager and Garbage collector

The memory manager doesn’t “know” which objects are still referenced and which ones are phantom objects, so it will only place objects in free spaces in memory. We rely on the garbage collector to identify and clean out phantom memory objects. When called into action, the garbage collector looks through all the memory objects and determines which items are no longer used. Then it labels these memory spaces as free to make them usable by the memory manager. 

The memory manager and garbage collector have separate jobs, but they work together manage the memory space for your variables.

Calling the garbage collector

As mentioned above, the memory manager only knows about open memory spaces and creates variables there. But remember, if the memory manager finds enough available space anywhere, it will place it there and move on. However, if the memory manager cannot find space for a new object, it immediately calls in the garbage collector to clean out any unused memory items. Then the memory manager will search one more time for enough memory space to store the new object.

The garbage collector is always ready and waiting to go to work, but it only cleans out the memory space when it receives an request from the memory manager, or from you!

In your CircuitPython code, you can request that the garbage collector clean out all the unused variables in volatile memory space by using the gc.collect() command. There is a built-in module called gc that you import and then you can use the gc.collect() command as below:

import gc

# some lines of code here...

gc.collect()

Example: Clean out the "phantom" Blinka bitmap

In the previous example, the references to the Blinka bitmap were removed, yet the memory manager looked past it and placed the time variable after the "50" score variable.

Immediately after de-referencing the Blinka bitmap is a great time to call the garbage collector.  After the Blinka's game is over, we immediately call gc.collect(). The garbage collector arrives to mark the unreferenced Blinka bitmap memory space as free and the memory manager can now place the time variable there.  

So, after large memory items get dereferenced, it's a great time to call in the garbage collector to free out those unused memory spaces.

After large memory items get dereferenced or after calling a function and the function returns, it's a great time to call in the garbage collector to free out unused memory spaces. So call gc.collect() anytime you are finished using large memory objects. If you are done with a variable you can call del large_variable. If nothing else is referencing that variable, a call to gc.collect() will free up that memory space for use in your code. 

This guide was first published on Apr 17, 2021. It was last updated on Apr 17, 2021.

This page (Memory: CircuitPython basics) was last updated on Nov 27, 2021.

Text editor powered by tinymce.