From 7c774699d7b610765c1a24ea3c9cc7fcbdbeb4f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Sep 2026 15:24:30 +0200 Subject: [PATCH] [api] Outline the fixed32 writers on ARM cores without unaligned access too Cortex-M0+ and ARM9 turn the four byte unaligned store into a memcpy call with a stack temporary at every fixed32 field, and the outlined helper itself became a memcpy call there, so the helper now spells out the byte stores. Xtensa and host objects are byte for byte unchanged; on the RP2040 bench config the api object loses 28 bytes and the fixed32 memcpy calls. --- esphome/components/api/proto.h | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index e459b19dab..c29f46f0b4 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -287,13 +287,16 @@ class ProtoWriteBuffer { uint8_t *pos_; }; -// ESP32 builds pass -fno-builtin-memcpy, so the inline write was already a memcpy call there and -// one shared copy is both smaller and faster. Elsewhere the write inlines to a few stores, which is -// still cheaper than a call (measured on ESP8266), so keep it inline. -#ifdef USE_ESP32 +// A four byte unaligned store is a memcpy call on ESP-IDF (-fno-builtin-memcpy) and on ARM cores without +// unaligned access (Cortex-M0+, ARM9), so those targets share one outlined byte store helper per fixed32 +// field. Elsewhere the write inlines to a single store, or on ESP8266 to a few stores that measured +// faster than a call, so it stays inline. +#if defined(USE_ESP32) || (defined(__arm__) && !defined(__ARM_FEATURE_UNALIGNED)) #define PROTO_OUTLINE_FOR_SIZE __attribute__((noinline)) +#define PROTO_FIXED32_BYTE_STORES true #else #define PROTO_OUTLINE_FOR_SIZE inline +#define PROTO_FIXED32_BYTE_STORES false #endif // Varint encoding thresholds — used by both proto_encode_* free functions and ProtoSize. @@ -417,8 +420,16 @@ class ProtoEncode { } /// Unaligned little-endian store; __builtin_memcpy stays inline even under -fno-builtin-memcpy. static inline void ESPHOME_ALWAYS_INLINE write_fixed32_le(uint8_t *__restrict__ pos, uint32_t value) { - const uint32_t le = convert_little_endian(value); - __builtin_memcpy(pos, &le, 4); + if constexpr (PROTO_FIXED32_BYTE_STORES) { + // Spelled out so the outlined helper does not itself become a memcpy call + pos[0] = static_cast(value); + pos[1] = static_cast(value >> 8); + pos[2] = static_cast(value >> 16); + pos[3] = static_cast(value >> 24); + } else { + const uint32_t le = convert_little_endian(value); + __builtin_memcpy(pos, &le, 4); + } } /// Write a precomputed tag byte + 32-bit value. Outlined on embedded: one copy beats inline stores per field. [[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *write_tag_and_fixed32( @@ -585,6 +596,7 @@ class ProtoEncode { } }; #undef PROTO_OUTLINE_FOR_SIZE +#undef PROTO_FIXED32_BYTE_STORES #ifdef HAS_PROTO_MESSAGE_DUMP /**