Python tooling for dependency-aware function code size analysis in embedded firmware — parses
.mapfiles andobjdumpdisassembly to compute the true binary footprint of any function including all transitive dependencies, for both ARM and RISC-V targets.
Developed during Master's thesis at TU Chemnitz / Infineon Technologies (2025).
Used to measure code size across 20+ CMSIS-DSP/NN and NMSIS/Andes-DSP/NN kernels.
Source repos:
- ARM benchmarks: ARM-Project
- RISC-V benchmarks: RISCV-Project
- Full results: arm-riscv-benchmark-results
A common mistake in embedded benchmarking is reporting only a function's own compiled size. In practice, integrating a kernel means paying for every routine it calls — helper functions, math utilities, initialisation code.
For example, arm_cfft_f32 itself is only 308 bytes but its full footprint
including all dependencies is 3308 bytes — more than 10× larger.
Reporting only 308 bytes would mislead any developer choosing between CMSIS and NMSIS
under tight Flash constraints.
These scripts always report both figures — own size and total size with full dependency breakdown.
Parses ARM .map files and objdump disassembly (using bl, blx, b.w call instructions)
to build a call graph and compute total function footprint.
python function_size.py <map_file> <objdump_file> <function_name> [function_name2 ...]Example:
python function_size.py \
Release/arm_fft_benchmark.map \
Release/arm_fft_benchmark.objdump \
arm_cfft_f32 arm_cfft_q15Output:
[*] Analyzing file: Release/arm_fft_benchmark.map
[*] Function: arm_cfft_f32
Own size: 308 bytes
Total size (with dependencies): 3308 bytes
[*] Breakdown:
arm_radix8_butterfly_f32 1404 bytes
arm_cfft_radix8by4_f32 1176 bytes
arm_cfft_radix8by2_f32 420 bytes
arm_cfft_f32 308 bytes
------------------------------------------------------------
Same analysis for RISC-V targets — parses jal, j, c.j, call instructions
from RISC-V objdump disassembly (RVC compressed encoding supported).
python function_size_riscv.py <map_file> <objdump_file> <function_name> [function_name2 ...]Example:
python function_size_riscv.py \
output/map.txt \
output/objdump.txt \
riscv_dsp_cfft_f32 riscv_dsp_cfft_q15Output:
[*] Analyzing file: output/map.txt
[*] Function: riscv_dsp_cfft_f32
Own size: 272 bytes
Total size (with dependencies): 2184 bytes
[*] Breakdown:
riscv_dsp_cfft_rd4_f32 1052 bytes
riscv_dsp_cfft_rd2_f32 624 bytes
riscv_dsp_cfft_f32 272 bytes
riscv_dsp_bitreversal 236 bytes
------------------------------------------------------------
Both scripts are designed to run automatically as post-build steps inside the project Makefile — no manual invocation needed after each build.
Add this target to your project Makefile:
.PHONY: function-analysis
FUNCTIONS := riscv_dsp_cfft_f32 riscv_dsp_cfft_q15 \
riscv_dsp_cifft_f32 riscv_dsp_cifft_q15
function-analysis: output/objdump.txt output/map.txt
@echo "Running function size analysis..."
python tools/function_size_riscv.py \
output/map.txt \
output/objdump.txt \
$(FUNCTIONS) > tools/code_size_report.txt || trueAfter each build, run:
make function-analysisOutput is saved to tools/code_size_report.txt.
Add this to your ModusToolbox Makefile as a POSTBUILD hook —
runs automatically every time the project builds successfully:
TARGET_FUNC := arm_cfft_f32 arm_cfft_q15 arm_fir_f32
POSTBUILD=\
$(MTB_TOOLCHAIN_GCC_ARM__BASE_DIR)/bin/arm-none-eabi-objdump -S \
$(MTB_TOOLS__OUTPUT_CONFIG_DIR)/$(APPNAME).$(MTB_TOOLCHAIN_GCC_ARM__SUFFIX_TARGET) \
> $(MTB_TOOLS__OUTPUT_CONFIG_DIR)/$(APPNAME).objdump && \
python tools/function_size.py \
$(MTB_TOOLS__OUTPUT_CONFIG_DIR)/$(APPNAME).map \
$(MTB_TOOLS__OUTPUT_CONFIG_DIR)/$(APPNAME).objdump \
$(TARGET_FUNC) > tools/code_size_report.txtThe POSTBUILD variable in ModusToolbox executes after a successful build —
objdump is generated automatically and the analysis runs immediately,
saving results to tools/code_size_report.txt.
Both scripts follow the same 3-step pipeline:
1. Parse .map file
└─► Extract function names + sizes from .text.* sections
2. Parse objdump disassembly
└─► Build call graph: which functions call which
ARM: bl / blx / b.w instructions
RISC-V: jal / j / c.j / call instructions
3. BFS traversal from root function
└─► Visit all reachable callees
└─► Sum sizes of all unique visited functions
└─► Report: own size + total size + breakdown
function_size.py (ARM) |
function_size_riscv.py (RISC-V) |
|
|---|---|---|
| Call instructions parsed | bl, blx, b.w, b |
jal, j, c.j, call |
| Compressed encoding | No (ARM Thumb) | Yes (RISC-V RVC) |
| Map section format | .text.arm_* |
.text.riscv_* |
Python >= 3.8
No external dependencies — uses only Python standard library (sys, re, os,
collections). No pip install needed.
mcu-function-size-analyser/
├── function_size.py # ARM Cortex-M code size analyser
├── function_size_riscv.py # RISC-V Andes D25F code size analyser
└── README.md
- Copy
function_size.py(ARM) orfunction_size_riscv.py(RISC-V) into atools/folder in your project - Add the Makefile target shown above
- Set your target function names in
FUNCTIONS/TARGET_FUNC - Build your project — the report generates automatically
| Repo | Description |
|---|---|
| ARM-Project | ARM Cortex-M4 CMSIS-DSP/NN benchmarks — uses function_size.py |
| RISCV-Project | RISC-V NMSIS-DSP/NN benchmarks — uses function_size_riscv.py |
| arm-riscv-benchmark-results | Full cross-architecture results including code size comparisons |
Developed as part of a Master's thesis at Technische Universität Chemnitz in collaboration with Infineon Technologies, Dresden (2025).
Karthik Swaminathan — Embedded Firmware Engineer
M.Sc. Embedded Systems · TU Chemnitz
LinkedIn · karthik94870@gmail.com