From 29dcf9fc5176a84bcf27e98411c5a665028af7c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Apr 2026 14:13:40 -0500 Subject: [PATCH] [core] Use __attribute__((noinline)) on IIFE lambdas to honor attribute The C++ standard-attribute spelling [[gnu::noinline]] placed between a lambda's parameter list and body binds to the return type, not the call operator. GCC 14 silently ignores it and emits -Wattributes warnings at every chunk site. Switch to GCC's __attribute__((...)) syntax which binds to operator() as intended. Measured impact on apollo-r-pro-1-eth (esp32-s3, -Os) vs the broken [[gnu::noinline]] version: setup() frame 160 B -> 32 B, peak stack 304 B -> 176 B (another -42%). Flash grows by 888 B because all 86 chunks now stay as separate functions instead of GCC inlining the small ones (which it was free to do when the attribute was ignored). Net vs baseline -Os: peak stack 1264 B -> 176 B (-86%); flash +388 B (<0.05% of a typical esp32 partition). --- esphome/core/__init__.py | 9 +++++++-- tests/unit_tests/test_core.py | 16 ++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 43c1ffaed0..8c44964cac 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -532,12 +532,17 @@ class Library: def _wrap_in_noinline_iifes(lines: list[str], max_statements: int) -> list[str]: - """Wrap ``lines`` in one or more ``[]() [[gnu::noinline]] { ... }();`` IIFEs. + """Wrap ``lines`` in one or more ``[]() __attribute__((noinline)) {...}();`` 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. """ out: list[str] = [] chunk: list[str] = [] @@ -546,7 +551,7 @@ def _wrap_in_noinline_iifes(lines: list[str], max_statements: int) -> list[str]: def flush() -> None: if not chunk: return - out.append("[]() [[gnu::noinline]] {") + out.append("[]() __attribute__((noinline)) {") out.extend(chunk) out.append("}();") chunk.clear() diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 1c31f24066..1e4f129896 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -877,7 +877,7 @@ def test_wrap_in_noinline_iifes_empty_input() -> None: def test_wrap_in_noinline_iifes_fewer_lines_than_limit() -> None: lines = ["a();", "b();", "c();"] assert core._wrap_in_noinline_iifes(lines, max_statements=10) == [ - "[]() [[gnu::noinline]] {", + "[]() __attribute__((noinline)) {", "a();", "b();", "c();", @@ -896,13 +896,13 @@ def test_wrap_in_noinline_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) == [ - "[]() [[gnu::noinline]] {", + "[]() __attribute__((noinline)) {", "a();", "{", "inner();", "}", "}();", - "[]() [[gnu::noinline]] {", + "[]() __attribute__((noinline)) {", "b();", "}();", ] @@ -911,14 +911,14 @@ def test_wrap_in_noinline_iifes_never_splits_inside_braces() -> None: def test_wrap_in_noinline_iifes_nested_braces() -> None: lines = ["{", "{", "deep();", "}", "}", "after();"] assert core._wrap_in_noinline_iifes(lines, max_statements=1) == [ - "[]() [[gnu::noinline]] {", + "[]() __attribute__((noinline)) {", "{", "{", "deep();", "}", "}", "}();", - "[]() [[gnu::noinline]] {", + "[]() __attribute__((noinline)) {", "after();", "}();", ] @@ -929,7 +929,7 @@ def test_wrap_in_noinline_iifes_unbalanced_braces_fall_through() -> None: # 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] == "[]() [[gnu::noinline]] {" + assert result[0] == "[]() __attribute__((noinline)) {" 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("[]() [[gnu::noinline]] {") == 2 + assert out.count("[]() __attribute__((noinline)) {") == 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("[]() [[gnu::noinline]] {") + assert out.index("prefix();") < out.index("[]() __attribute__((noinline)) {")