py/emitglue: Reduce RAM footprint of frozen code and SYS_SETTRACE feature by yoctopuce · Pull Request #18346 · micropython/micropython · GitHub
Skip to content

py/emitglue: Reduce RAM footprint of frozen code and SYS_SETTRACE feature - #18346

Open
yoctopuce wants to merge 3 commits into
micropython:masterfrom
yoctopuce:frozen_mp_obj_fun_bc
Open

yoctopuce wants to merge 3 commits into
micropython:masterfrom
yoctopuce:frozen_mp_obj_fun_bc

Conversation

@yoctopuce

@yoctopuce yoctopuce commented Oct 30, 2025

Copy link
Copy Markdown
Contributor

Summary

My MicroPython port has relatively large flash storage, but not much RAM (only 100 KB is available for MicroPython heap). Therefore I rely on frozen modules to save RAM. My port includes SYS_SETTRACE to allow the end user to debug their own code using Visual Studio Code.

Examining my runtime heap map in detail, I realised that frozen functions use a significant amount of RAM: 32 bytes per function or method. This equated to 20 KB of RAM when importing my main library.

Further investigation revealed a significant waste of ROM and RAM when enabling SYS_SETTRACE. This pull request attempts to address this issue:

  • the first commit reduces the footprint of the mp_raw_code_t structure, which uses unnecessary ROM space in frozen code, but also uses RAM for dynamically loaded functions. Depending on the port options, this may also reduce the code footprint for builds not using SYS_SETTRACE.
  • the second commit solves the problem of mp_obj_fun_bc_t objects using two GC units instead of one when SYS_SETTRACE is enabled. This requires the use of a different structure for each kind of function object, instead of just two types as it is currently the case, so some assumptions about the usage of function objects had to be made more explicit.
  • the third commit goes one step further by adding support for frozen mp_obj_fun_bc_t objects, not using any RAM. This improvement may benefit to any port with frozen modules, even when SYS_SETTRACE is not enabled. The price to pay for this is of course a bit of ROM, and the use of an indirect pointer to load the module globals dictionary.

edit on 2025-10-31: this third commit has been updated based on the alternative implementation that I had considered in my first submit, i.e. frozing the mp_module_context_t object as well, with just the pointer to the module-specific globals dictionary in RAM. This results in simpler code so I think this is preferable. The first version is still available on my repository for reference if needed.

Testing

The code has been extensively tested on my port, executing both frozen and runtime code.

I started from a build with 185 KB of rodata for frozen modules. Loading my main frozen library used 36KB RAM (for defining classes involving about 600 methods). After the optimizations, I ended up with 162 KB of rodata for frozen modules, and only 17KB RAM used after loading my main library.

So the total savings in my case are 20 KB flash, 20 KB RAM.

I have enabled the new features for some configurations of CI tests to be sure they are covered.

Trade-offs and Alternatives

There are other ways to somehow reduce the RAM footprint of frozen modules, such as lazy loading (as done in asyncio for instance). I have gone that way first, but ultimately:

  • there was still way to much RAM wasted once I had loaded the libraries that I needed
  • the lazy loading solution tends to defeat type checking algorithms used by IDE
  • the lazy loading solution breaks if the user uses a star import
  • cross references between multiple classes is really tricky to implement properly with the lazy loading method
    So ultimately, freezing mp_obj_fun_bc_t objects was a much cleaner solution.

When a port defines MICROPY_PY_SYS_SETTRACE, there is currently a
large overhead in memory usage due to the extra pointers required
to access code information for tracing. This overhead exists even
when tracing is not active. It impacts both RAM and frozen code
in flash memory.

This commit reduces this overhead by:
- removing unused fields in the profiling data structure
- optimizing the size of mp_raw_code_t, which is a temporary
  structure for normal use but is persistent when SYS_SETTRACE
  is enabled (and also impacts frozen code size).

As a side effect, this commit also reduce the code footprint of
frozen functions when MICROPY_PERSISTENT_CODE_SAVE is enabled,
even for builds without SYS_SETTRACE enabled.

Signed-off-by: Yoctopuce dev <dev@yoctopuce.com>
@codecov

codecov Bot commented Oct 30, 2025

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Oct 30, 2025

Copy link
Copy Markdown

Code size report:

Reference:  stm32/usb: Add VBUS sensing configuration for TinyUSB on F4/F7. [27b7bf3]
Comparison: tools/mpy-tool: Allow frozen function objects in frozen modules. [merge of 6849d21]
  mpy-cross:   +16 +0.004% 
   bare-arm:   +16 +0.028% 
minimal x86:   -13 -0.007% 
   unix x64:   +72 +0.008% standard
      stm32:   +80 +0.020% PYBV10
     mimxrt:   +80 +0.021% TEENSY40
        rp2:   +56 +0.006% RPI_PICO_W
       samd:   +72 +0.026% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:   +50 +0.011% VIRT_RV32

When a port defines MICROPY_PY_SYS_SETTRACE, there is currently a
large overhead in memory usage due to the mp_obj_fun_bc_t object
taking 2 GC units per function instead of 1. This can be avoided
by removing redundant information from mp_obj_fun_bc_t when the
function kind is BYTECODE and SYS_SETTRACE is enabled.

This requires however to make the storage underlying mp_obj_fun_*
objects different for bytecode and native/viper function objects.
There is currently a grey zone with regard to the exact underlying
data structure for function objects: inline_asm functions have
their own mp_obj_fun_asm_t structure, but native and viper functions
currently use the mp_obj_fun_bc_t structure, although they are
not really bytecode. This commit make them explicitly different
to increase the code readability, and clarify assumptions in code.

Signed-off-by: Yoctopuce dev <dev@yoctopuce.com>
Currently, loading a frozen module with many functions and
methods results in substantial RAM usage due to the creation
function objects (one GC block is created for each function
or method defined in the module).

This commit provides a way to freeze the bytecode function
objects in ROM, along with the raw_code objects. In order to
achieve this, the module context structure must also be frozen,
to allow frozen bytecode functions to reference it. For
frozen module contexts, the module-specific globals dict is
therefore referenced using an indirect pointer in RAM.

Signed-off-by: Yoctopuce dev <dev@yoctopuce.com>
@yoctopuce
yoctopuce force-pushed the frozen_mp_obj_fun_bc branch from de19fdb to 6849d21 Compare October 31, 2025 18:25
@dpgeorge dpgeorge added the py-core Relates to py/ directory in source label Nov 3, 2025
@dpgeorge

Copy link
Copy Markdown
Member

Thanks very much for this, it looks very interesting! I will make some time to review it in detail.

In the meantime, I did something closely related but orthorgonal in #18571 that you may find useful for your application.

@dpgeorge dpgeorge added this to the release-1.28.0 milestone Dec 15, 2025
@yoctopuce

Copy link
Copy Markdown
Contributor Author

In the meantime, I did something closely related but orthorgonal in #18571 that you may find useful for your application.

Indeed, this looks indeed very interesting ! I will try to integrate it and see if I get the performance benefit.

@dpgeorge

Copy link
Copy Markdown
Member

There is a lot going on in this PR. Would it make sense to split out the commits into separate PRs to review them, and also check their impact on code size / RAM use ? In particular the first commit (reworking mp_raw_code_t) seems to be quite self contained.

A general thing that would be good to discuss, especially in relation to that first commit: for bytecode functions, it's possible to remove the fun_data_len and n_children (and maybe other entries) from mp_raw_code_t and compute them on the fly when they are needed. That trades RAM/ROM storage for computation time, so it's a tradeoff either way. In your case would you prefer to regain RAM and ROM at the expense of settrace functions being slower?

@yoctopuce

Copy link
Copy Markdown
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

py-core Relates to py/ directory in source

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants