8000 8000
Skip to content

[esp32] Move heap functions to flash, saving ~6KB - #12862

Merged
bdraco merged 1 commit into
devfrom
heap-in-iram-flash
Jan 4, 2026
Merged

[esp32] Move heap functions to flash, saving ~6KB#12862
bdraco merged 1 commit into
devfrom
heap-in-iram-flash

Conversation

@bdraco
@bdraco bdraco commented Jan 4, 2026
Copy link
Copy Markdown
Member

What does this implement/fix?

This is the culmination of months of work to reduce heap churn throughout the ESPHome codebase. By systematically eliminating unnecessary dynamic allocations, we've reached a point where heap functions are called so infrequently that they can safely be moved from IRAM to flash - unlocking ~6 KB of additional heap memory essentially for free.

Background

Over the past several months, we've made extensive optimizations to reduce heap churn:

  • Introduced StaticVector and FixedVector to replace std::vector where sizes are known
  • Replaced std::string with const char* for static strings
  • Pre-allocated buffers instead of dynamic allocation in hot paths
  • Moved to stack allocation where possible
  • Eliminated temporary allocations in loops
  • Audio/video components use pre-allocated ring buffers, not dynamic allocation during streaming

The result: heap functions (malloc, free, realloc) are now primarily called during setup, not during normal operation. This makes the performance difference between IRAM and flash access negligible.

Even without the heap churn reduction work, the impact would be minimal - the overhead only occurs on cache misses, and with heavy heap usage the functions would likely stay cached anyway.

The Optimization

Enable CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH by default, moving heap functions from IRAM to flash.

This is safe because:

  1. Heap functions should never be called from ISRs (and ESPHome doesn't do this)
  2. CONFIG_SPI_MASTER_ISR_IN_IRAM is not enabled
  3. The heap churn reduction work means allocations are infrequent

Measured results:

  • Heap free before: 315,596 bytes
  • Heap free after: 321,720 bytes
  • Savings: +6,124 bytes (~6 KB)

An advanced: heap_in_iram escape hatch is provided for users who need heap functions in IRAM for specific use cases.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Developer breaking change (an API change that could break external components)
  • Code quality improvements to existing code or addition of tests
  • Other

Related issue or feature (if applicable):

  • n/a

Pull request in esphome-docs with documentation (if applicable):< 8000 /p>

Test Environment

  • ESP32
  • ESP32 IDF
  • ESP8266
  • RP2040
  • BK72xx
  • RTL87xx
  • nRF52840

Example entry for config.yaml:

# Default behavior - heap functions in flash (recommended)
esp32:
  board: esp32dev
  framework:
    type: esp-idf

# Escape hatch - keep heap functions in IRAM if needed
esp32:
  board: esp32dev
  framework:
    type: esp-idf
    advanced:
      heap_in_iram: true

Checklist:

  • The code change is tested and works locally.
  • Tests have been added to verify that the new code works (under tests/ folder).

If user exposed functionality or configuration variables are added/changed:

This is the culmination of months of work to reduce heap churn
throughout the ESPHome codebase. By systematically eliminating
unnecessary dynamic allocations (StaticVector, FixedVector, const
char* instead of std::string, pre-allocated buffers, etc.), heap
functions are now called so infrequently that they can safely be
moved from IRAM to flash.

Enable CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH by default, which moves
malloc/free/realloc from IRAM to flash. This is safe because:
- Heap functions should never be called from ISRs
- CONFIG_SPI_MASTER_ISR_IN_IRAM is not enabled
- Audio/video use pre-allocated ring buffers, not dynamic allocation

Measured results: +6,124 bytes of heap freed.

Add heap_in_iram advanced option as an escape hatch for users who
need heap functions in IRAM for specific use cases.
@github-actions
github-actions Bot commented Jan 4, 2026
Copy link
Copy Markdown
Contributor

To use the changes from this PR as an external component, add the following to your ESPHome configuration YAML file:

external_components:
  - source: github://pr#12862
    components: [esp32]
    refresh: 1h

(Added by the PR bot)

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.44%. Comparing base (f11abc7) to head (ba1bbaf).
⚠️ Report is 16 commits behind head on dev.

Additional details and impacted files
@@           Coverage Diff           @@
##              dev   #12862   +/-   ##
=======================================
  Coverage   73.44%   73.44%           
=======================================
  Files          53       53           
  Lines       11303    11303           
  Branches     1534     1534           
=======================================
  Hits         8302     8302           
  Misses       2602     2602           
  Partials      399      399           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
github-actions Bot commented Jan 4, 2026
Copy link
Copy Markdown
Contributor

Memory Impact Analysis

Components: esp32
Platform: esp32-idf

Metric Target Branch This PR Change
RAM 46,292 bytes 46,292 bytes ➡️ +0 bytes (0.00%)
Flash 887,226 bytes 886,818 bytes 📉 ✅ -408 bytes (-0.05%)
📊 Component Memory Breakdown
Component Target Flash PR Flash Change
wifi_stack 107,572 bytes 107,473 bytes 📉 -99 bytes (-0.09%)
app_framework 5,992 bytes 5,960 bytes 📉 -32 bytes (-0.53%)
crypto 25,847 bytes 25,823 bytes 📉 -24 bytes (-0.09%)
heap 2,742 bytes 2,726 bytes 📉 -16 bytes (-0.58%)
memory_mgmt 11,402 bytes 11,386 bytes 📉 -16 bytes (-0.14%)
math_lib 3,229 bytes 3,213 bytes 📉 -16 bytes (-0.50%)
time_lib 2,961 bytes 2,946 bytes 📉 -15 bytes (-0.51%)
phy_radio 32,156 bytes 32,141 bytes 📉 -15 bytes (-0.05%)
cache_mgmt 2,185 bytes 2,170 bytes 📉 -15 bytes (-0.69%)
freertos 17,322 bytes 17,307 bytes 📉 -15 bytes (-0.09%)
hw_crypto 2,827 bytes 2,815 bytes 📉 -12 bytes (-0.42%)
wifi_crypto 6,740 bytes 6,728 bytes 📉 -12 bytes (-0.18%)
other 18,716 bytes 18,704 bytes 📉 -12 bytes (-0.06%)
crypto_math 25,144 bytes 25,136 bytes 📉 -8 bytes (-0.03%)
wrapper_functions 418 bytes 410 bytes 📉 -8 bytes (-1.91%)
vfs 1,081 bytes 1,089 bytes 📈 +8 bytes (+0.74%)
rom_functions 13,454 bytes 13,462 bytes 📈 +8 bytes (+0.06%)
ieee802_11 209 bytes 201 bytes 📉 -8 bytes (-3.83%)
wifi_phy_pp 9,380 bytes 9,384 bytes 📈 +4 bytes (+0.04%)
base_node 170 bytes 166 bytes 📉 -4 bytes (-2.35%)
... ... ... (5 more components not shown)
🔍 Symbol-Level Changes (click to expand)

Changed Symbols

Symbol Target Size PR Size Change
crypto_ecdh_set_peerkey 319 bytes 303 bytes 📉 -16 bytes (-5.02%)
multi_heap_get_info 102 bytes 86 bytes 📉 -16 bytes (-15.69%)
wpa_config_parse_string 246 bytes 230 bytes 📉 -16 bytes (-6.50%)
esp_phy_load_cal_and_init 266 bytes 251 bytes 📉 -15 bytes (-5.64%)
owe_process_assoc_resp 465 bytes 453 bytes 📉 -12 bytes (-2.58%)
sae_derive_pwe_ecc 889 bytes 877 bytes 📉 -12 bytes (-1.35%)
handler_instances_add 114 bytes 102 bytes 📉 -12 bytes (-10.53%)
xRingbufferCreateWithCaps 81 bytes 93 bytes 📈 +12 bytes (+14.81%)
eloop_register_timeout 252 bytes 241 bytes 📉 -11 bytes (-4.37%)
ieee802_11_parse_elems 209 bytes 201 bytes 📉 -8 bytes (-3.83%)
wpa_sm_deinit 63 bytes 55 bytes 📉 -8 bytes (-12.70%)
esp_event_loop_create 290 bytes 282 bytes 📉 -8 bytes (-2.76%)
loop_remove_handler 267 bytes 259 bytes 📉 -8 bytes (-3.00%)
loop_node_add_handler 227 bytes 219 bytes 📉 -8 bytes (-3.52%)
vQueueDeleteWithCaps 59 bytes 51 bytes 📉 8000 -8 bytes (-13.56%)
ccmp_decrypt 143 bytes 135 bytes 📉 -8 bytes (-5.59%)
esp_vfs_register_common 216 bytes 224 bytes 📈 +8 bytes (+3.70%)
esp_aes_gcm_setkey 113 bytes 105 bytes 📉 -8 bytes (-7.08%)
esp_vfs_register_fd_range 194 bytes 202 bytes 📈 +8 bytes (+4.12%)
xQueueCreateWithCaps 85 bytes 77 bytes 📉 -8 bytes (-9.41%)
wpa_supplicant_process_1_of_4 339 bytes 331 bytes 📉 -8 bytes (-2.36%)
esp_event_loop_run 463 bytes 455 bytes 📉 -8 bytes (-1.73%)
esp_vfs_open 248 bytes 256 bytes 📈 +8 bytes (+3.23%)
crypto_ec_key_verify_signature 98 bytes 90 bytes 📉 -8 bytes (-8.16%)
esp_mmu_map 974 bytes 966 bytes 📉 -8 bytes (-0.82%)
pmksa_cache_deinit 53 bytes 46 bytes 📉 -7 bytes (-13.21%)
xSemaphoreCreateGenericWithCaps 96 bytes 89 bytes 📉 -7 bytes (-7.29%)
crypto_bignum_init_uint 46 bytes 42 bytes 📉 -4 bytes (-8.70%)
vSemaphoreDeleteWithCaps 51 bytes 47 bytes 📉 -4 bytes (-7.84%)
vRingbufferDelete 47 bytes 43 bytes 📉 -4 bytes (-8.51%)
... ... ... (39 more changed symbols not shown)

Note: This analysis measures static RAM and Flash usage only (compile-time allocation).
Dynamic memory (heap) cannot be measured automatically.
⚠️ You must test this PR on a real device to measure free heap and ensure no runtime memory issues.

This analysis runs automatically when components change. Memory usage is measured from a representative test configuration.

@bdraco bdraco removed the needs-tests label Jan 4, 2026
@esphome esphome Bot added code-quality and removed new-feature labels Jan 4, 2026
@bdraco
bdraco marked this pull request as ready for review January 4, 2026 01:22
@bdraco
bdraco requested a review from a team as a code owner January 4, 2026 01:22
Copilot AI review requested due to automatic review settings January 4, 2026 01:22
@github-actions
github-actions Bot commented Jan 4, 2026
Copy link
Copy Markdown
Contributor

👋 Hi there! This PR modifies 1 file(s) with codeowners.

@esphome/core - As codeowner(s) of the affected files, your review would be appreciated! 🙏

Note: Automatic review request may have failed, but you're still welcome to review.

Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes ESP32 IRAM usage by moving heap functions (malloc, free, realloc) from IRAM to flash memory, freeing up approximately 6KB of heap memory. This optimization is made possible by extensive heap churn reduction work throughout the ESPHome codebase, which has minimized the frequency of heap allocations during normal operation. The change introduces a new heap_in_iram configuration option (defaulting to false) as an escape hatch for users who need heap functions in IRAM.

Key Changes

  • Adds CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH configuration to move heap functions from IRAM to flash by default
  • Introduces heap_in_iram advanced configuration option to allow users to revert to the old behavior if needed
  • Measured heap savings: 6,124 bytes (~6KB)

Comment thread esphome/components/esp32/__init__.py
Comment thread esphome/components/esp32/__init__.py
@bdraco
bdraco commented Jan 4, 2026
Copy link
Copy Markdown
Member Author

Thanks

@bdraco
bdraco merged commit 5d384c7 into dev Jan 4, 2026
104 of 105 checks passed
@bdraco
bdraco deleted the heap-in-iram-flash branch January 4, 2026 02:00
@github-actions github-actions Bot locked and limited conversation to collaborators Jan 6, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

0