Conversation
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>
|
Code size report: |
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>
de19fdb to
6849d21
Compare
|
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. |
Indeed, this looks indeed very interesting ! I will try to integrate it and see if I get the performance benefit. |
|
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 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 |

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:
mp_raw_code_tstructure, 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.mp_obj_fun_bc_tobjects 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.mp_obj_fun_bc_tobjects, 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_tobject 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:
So ultimately, freezing
mp_obj_fun_bc_tobjects was a much cleaner solution.