[core] Drop noinline from IIFE chunks and rename helper

Additional measurements showed GCC's -Os inliner re-inlines most IIFE
chunks back into setup() by choice, and the structural scoping alone
captures nearly all of the peak-stack benefit on esp32 without the
flash cost of forcing all chunks to stay as real functions.

Apollo (esp32-s3, -Os) with vs without noinline:
  peak setup stack     176 B (noinline)  vs  304 B (scope-only)
  flash delta         +388 B (noinline)  vs   -504 B (scope-only)
  chunks kept          86               vs    20

Issue #15796 is an LVGL-setup class of bug that has only surfaced on
esp32 after years in the field; the extra guarantee that noinline
provides is not worth the flash cost in practice. Also rename the
helper from _wrap_in_noinline_iifes to _wrap_in_iifes to match.
This commit is contained in:
J. Nick Koston
2026-04-17 15:06:42 -05:00
parent 29dcf9fc51
commit 6a7c9af870
2 changed files with 29 additions and 28 deletions
+9 -8
View File
@@ -531,18 +531,19 @@ class Library:
return self
def _wrap_in_noinline_iifes(lines: list[str], max_statements: int) -> list[str]:
"""Wrap ``lines`` in one or more ``[]() __attribute__((noinline)) {...}();`` IIFEs.
def _wrap_in_iifes(lines: list[str], max_statements: int) -> list[str]:
"""Wrap ``lines`` in one or more ``[]() {...}();`` IIFEs.
Splits into IIFEs of up to ``max_statements`` entries each. Never splits
inside a brace-balanced block (e.g. the ``{`` / ``}`` pair that
``cg.with_local_variable()`` emits around a scoped local), so an IIFE
may exceed ``max_statements`` when a block straddles the boundary.
GCC note: ``__attribute__((noinline))`` between the lambda parameter
list and body binds to ``operator()``. The C++ standard-attribute
spelling ``[[gnu::noinline]]`` in that position binds to the return
type instead and produces ``-Wattributes`` warnings.
The IIFEs intentionally have no ``noinline`` attribute: GCC's ``-Os``
inliner makes good decisions about which chunks to keep as functions
and which to re-inline, and forcing all chunks to stay as functions
costs flash without measurably improving peak stack on configs where
the scope structure is itself sufficient to bound live-range lifetimes.
"""
out: list[str] = []
chunk: list[str] = []
@@ -551,7 +552,7 @@ def _wrap_in_noinline_iifes(lines: list[str], max_statements: int) -> list[str]:
def flush() -> None:
if not chunk:
return
out.append("[]() __attribute__((noinline)) {")
out.append("[]() {")
out.extend(chunk)
out.append("}();")
chunk.clear()
@@ -1065,7 +1066,7 @@ class EsphomeCore:
# (e.g. sensor platforms with many filter registrations).
pieces = list(prefix)
for block in components:
pieces.extend(_wrap_in_noinline_iifes(block, max_statements=50))
pieces.extend(_wrap_in_iifes(block, max_statements=50))
return "\n".join(pieces) + "\n\n"
@property
+20 -20
View File
@@ -870,14 +870,14 @@ class TestEsphomeCore:
assert "Wire" in target.platformio_libraries
def test_wrap_in_noinline_iifes_empty_input() -> None:
assert not core._wrap_in_noinline_iifes([], max_statements=10)
def test_wrap_in_iifes_empty_input() -> None:
assert not core._wrap_in_iifes([], max_statements=10)
def test_wrap_in_noinline_iifes_fewer_lines_than_limit() -> None:
def test_wrap_in_iifes_fewer_lines_than_limit() -> None:
lines = ["a();", "b();", "c();"]
assert core._wrap_in_noinline_iifes(lines, max_statements=10) == [
"[]() __attribute__((noinline)) {",
assert core._wrap_in_iifes(lines, max_statements=10) == [
"[]() {",
"a();",
"b();",
"c();",
@@ -885,51 +885,51 @@ def test_wrap_in_noinline_iifes_fewer_lines_than_limit() -> None:
]
def test_wrap_in_noinline_iifes_splits_at_max_statements() -> None:
def test_wrap_in_iifes_splits_at_max_statements() -> None:
lines = [f"s{i}();" for i in range(5)]
result = core._wrap_in_noinline_iifes(lines, max_statements=2)
result = core._wrap_in_iifes(lines, max_statements=2)
# With max=2 and 5 lines: chunks of 2, 2, 1 → 3 IIFEs.
assert sum(1 for line in result if line.startswith("[]()")) == 3
def test_wrap_in_noinline_iifes_never_splits_inside_braces() -> None:
def test_wrap_in_iifes_never_splits_inside_braces() -> None:
# max=2 would naively split after "{" but brace guard keeps block whole.
lines = ["a();", "{", "inner();", "}", "b();"]
assert core._wrap_in_noinline_iifes(lines, max_statements=2) == [
"[]() __attribute__((noinline)) {",
assert core._wrap_in_iifes(lines, max_statements=2) == [
"[]() {",
"a();",
"{",
"inner();",
"}",
"}();",
"[]() __attribute__((noinline)) {",
"[]() {",
"b();",
"}();",
]
def test_wrap_in_noinline_iifes_nested_braces() -> None:
def test_wrap_in_iifes_nested_braces() -> None:
lines = ["{", "{", "deep();", "}", "}", "after();"]
assert core._wrap_in_noinline_iifes(lines, max_statements=1) == [
"[]() __attribute__((noinline)) {",
assert core._wrap_in_iifes(lines, max_statements=1) == [
"[]() {",
"{",
"{",
"deep();",
"}",
"}",
"}();",
"[]() __attribute__((noinline)) {",
"[]() {",
"after();",
"}();",
]
def test_wrap_in_noinline_iifes_unbalanced_braces_fall_through() -> None:
def test_wrap_in_iifes_unbalanced_braces_fall_through() -> None:
# Pathological input where "}" appears before "{": don't crash; emit
# a single IIFE with all lines rather than splitting mid-flight.
lines = ["a();", "}", "b();"]
result = core._wrap_in_noinline_iifes(lines, max_statements=1)
assert result[0] == "[]() __attribute__((noinline)) {"
result = core._wrap_in_iifes(lines, max_statements=1)
assert result[0] == "[]() {"
assert result[-1] == "}();"
assert [line for line in result if line in lines] == lines
@@ -952,7 +952,7 @@ def test_cpp_main_section_component_marker_wraps_in_iife() -> None:
RawStatement("new_wifi();"),
]
out = target.cpp_main_section
assert out.count("[]() __attribute__((noinline)) {") == 2
assert out.count("[]() {") == 2
assert out.count("}();") == 2
assert "// === logger ===" in out
assert "// === wifi ===" in out
@@ -966,4 +966,4 @@ def test_cpp_main_section_prefix_statements_stay_outside_iife() -> None:
RawStatement("body();"),
]
out = target.cpp_main_section
assert out.index("prefix();") < out.index("[]() __attribute__((noinline)) {")
assert out.index("prefix();") < out.index("[]() {")