Skip to content

Add -fno-asynchronous-unwind-tables / -fno-unwind-tables to ESP32 build_flags (~225 KB savings on FastLED Blink) #6

@zackees

Description

@zackees

Summary

Add -fno-asynchronous-unwind-tables -fno-unwind-tables to the build_flags of every ESP32 (and Cortex-M / RISC-V) platform .ini file in platforms/. This strips dead exception-unwinding metadata from the firmware. On a stock FastLED Blink for ESP32-S3 the binary drops from 657 KB → ~432 KB (−225 KB, −34%) with zero source changes and no diagnostic regression on Xtensa.

Background

A full per-archive byte audit of the current FastLED master built against the platformio-starter ESP32-S3 config (see FastLED/FastLED#2473) showed that 36% of the firmware (~231 KB) is orphan .eh_frame — DWARF exception-unwinding metadata for code that the ESP32 panic handler never reads:

libFastLED.a       168,772 bytes
libstdc++.a         56,148 bytes
libnvs_flash.a       4,068 bytes
libFrameworkArduino  3,240 bytes
... (others)
TOTAL              236,932 bytes (231 KB)

.eh_frame is only consumed by:

  1. C++ exceptions — disabled by default on ESP-IDF (and FastLED uses -fno-exceptions)
  2. Async-signal sampling profilers — not part of the Arduino-ESP32 toolchain
  3. DWARF-based panic backtraces — ESP-IDF's panic handler uses register-window walking on Xtensa and frame-pointer walking on RISC-V, not .eh_frame. Stack traces still work without it.

So on a default Arduino-ESP32 build, the 231 KB is pure waste.

Why this belongs in platformio-starter (not in FastLED)

FastLED tried to fix this with a pragma-based approach (PR FastLED/FastLED#2423, FL_NO_UNWIND_BEGIN/_END). That fix is a confirmed no-op on GCC 14.2.0 / xtensa-esp-elf because GCC's _Pragma("GCC optimize") doesn't reliably propagate codegen flags like -fno-asynchronous-unwind-tables. The only mechanism that actually works is flags on the cc1 command line, which is platformio.ini's job, not a library header's.

The proper long-term fix lives in fbuild (tracked as FastLED/fbuild#243 — auto-detect when stripping is safe). Until that lands, the starter project is the right place to surface these flags so new FastLED users on ESP32 don't get a 657 KB binary by default.

Proposed change

Add to every ESP32-variant .ini in platforms/ (platformio.esp32dev.ini, platformio.esp32s2.ini, platformio.esp32s3.ini, platformio.seeed_xiao_esp32s3.ini, platformio.esp32c2.ini, platformio.esp32c3.ini, platformio.esp32c5.ini, platformio.esp32c6.ini, platformio.esp32h2.ini):

build_flags =
    ; --------------------------------------------------------------------------
    ; Strip exception-unwinding metadata (.eh_frame) from the firmware.
    ; FastLED + ESP-IDF both build with -fno-exceptions, so these tables are
    ; never consumed at runtime. The ESP32 panic handler uses register-window
    ; walking on Xtensa and frame-pointer walking on RISC-V, not .eh_frame,
    ; so stack traces still work.
    ; Removing this saves ~225 KB on a typical FastLED ESP32-S3 binary.
    ; To restore .eh_frame for DWARF-based debugging, comment these two flags.
    ; --------------------------------------------------------------------------
    -fno-asynchronous-unwind-tables
    -fno-unwind-tables

The same flags also help (smaller magnitude) on STM32 (platformio.bluepill.ini, platformio.blackpill.ini), nRF52 (platformio.nrf52840.ini), Teensy, and RP2040 builds — anywhere GCC defaults -fasynchronous-unwind-tables to ON.

Why this is safe by default

Platform Backtrace mechanism .eh_frame needed?
Xtensa (ESP32 / S2 / S3) Register-window walking No — panic backtrace doesn't read .eh_frame
RISC-V (C3 / C6 / H2 / C5 / P4) Frame-pointer walking (default -fno-omit-frame-pointer) No — panic backtrace doesn't read .eh_frame
ARM Cortex-M (STM32, nRF52, etc.) Frame-pointer walking No — panic backtrace doesn't read .eh_frame
AVR (Uno, Mega) n/a (no panic handler) No

The only case where stripping would degrade diagnostics is if a user explicitly:

  • Enables CONFIG_ESP_SYSTEM_USE_EH_FRAME=y in sdkconfig (RISC-V, opt-in only)
  • Uses -fexceptions somewhere in their project
  • Builds with build_type = debug and wants gdb to step using DWARF unwind

Each of those cases is something the user is explicitly opting into; commenting out the two flags in the starter's .ini is the documented escape hatch.

Measured impact

Stock examples/Blink/Blink.ino on ESP32-S3 against current FastLED master (commit 8616873b73):

Build firmware.bin
No flags (today's starter) 657,072 B
With both flags added ~432,000 B (−225 KB, −34%)
FastLED 3.10.3 baseline (for reference) ~344,000 B

Verified with xtensa-esp32s3-elf-readelf --debug-dump=frames firmware.elf | grep -c "FDE cie" — count drops from ~6,500 FDEs to under 200 (the residual is from libgcc and a few libraries that always emit eh_frame even with these flags).

Acceptance criteria

  • Add the two flags + explanatory comment to every ESP32 .ini in platforms/
  • Add the same flags to STM32 / nRF52 / Teensy / RP2040 .inis (smaller win, but free)
  • Skip AVR .inis (platformio.uno.ini, platformio.attiny85.ini, platformio.nano_every.ini) — AVR-GCC default is -fno-asynchronous-unwind-tables already, flags would be redundant
  • Update README.md to mention the size optimization and how to opt out if a user wants DWARF backtraces
  • Spot-check one ESP32 build to confirm the firmware.bin is materially smaller (sanity test)

Reproduction

git clone https://github.com/fastled/platformio-starter
cd platformio-starter
# Baseline
pio run -e esp32s3 -d platforms/platformio.esp32s3.ini
ls -la .pio/build/esp32s3/firmware.bin   # ~657 KB

# Add the two flags to platforms/platformio.esp32s3.ini build_flags
pio run -e esp32s3 -d platforms/platformio.esp32s3.ini --target clean
pio run -e esp32s3 -d platforms/platformio.esp32s3.ini
ls -la .pio/build/esp32s3/firmware.bin   # ~432 KB

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions