From 05896c38e88132903cbdd5fbfd33fb673ed2b479 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 15:20:58 -1000 Subject: [PATCH] [api] Inline ProtoSize::varint fast path for hot loop performance Split ProtoSize::varint() into an always-inlined fast path (value < 128) and a noinline slow path, eliminating indirect function calls on the BLE advertisement encode hot path. Also force-inline calc_uint32() and calc_length() so the compiler fully inlines size calculations per advertisement instead of emitting out-of-line calls. Verified via Xtensa disassembly: eliminates ~80 indirect function calls per 16-advertisement batch flush (3 calc + varint per advertisement). Total hot path code reduced from 24 to 21 functions. --- esphome/components/api/proto.cpp | 13 +++++++++++++ esphome/components/api/proto.h | 32 ++++++++++++++++---------------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 1ca6b702ad..6348c74c5f 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -8,6 +8,19 @@ namespace esphome::api { static const char *const TAG = "api.proto"; +uint32_t ProtoSize::varint_slow_(uint32_t value) { + // value is guaranteed >= 128 here (fast path handled inline) + if (value < 16384) { + return 2; // 14 bits + } else if (value < 2097152) { + return 3; // 21 bits + } else if (value < 268435456) { + return 4; // 28 bits + } else { + return 5; // 32 bits (maximum for uint32_t) + } +} + #ifdef USE_API_VARINT64 optional ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, uint32_t result32) { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index bbdd11b29d..33c7252220 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -511,23 +511,23 @@ class ProtoSize { * @param value The uint32_t value to calculate size for * @return The number of bytes needed to encode the value */ - static constexpr uint32_t varint(uint32_t value) { - // Optimized varint size calculation using leading zeros - // Each 7 bits requires one byte in the varint encoding + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) { if (value < 128) - return 1; // 7 bits, common case for small values - - // For larger values, count bytes needed based on the position of the highest bit set - if (value < 16384) { - return 2; // 14 bits - } else if (value < 2097152) { - return 3; // 21 bits - } else if (value < 268435456) { - return 4; // 28 bits - } else { - return 5; // 32 bits (maximum for uint32_t) + return 1; // Fast path: 7 bits, most common case + if (__builtin_is_constant_evaluated()) { + // Compile-time: full cascade for constexpr callers + if (value < 16384) + return 2; + if (value < 2097152) + return 3; + if (value < 268435456) + return 4; + return 5; } + return varint_slow_(value); } + // Slow path for varint >= 128, outlined to keep fast path small + static uint32_t varint_slow_(uint32_t value) __attribute__((noinline)); /** * @brief Calculates the size in bytes needed to encode a uint64_t value as a varint @@ -609,7 +609,7 @@ class ProtoSize { static constexpr uint32_t calc_int32_force(uint32_t field_id_size, int32_t value) { return field_id_size + (value < 0 ? 10 : varint(static_cast(value))); } - static constexpr uint32_t calc_uint32(uint32_t field_id_size, uint32_t value) { + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint32(uint32_t field_id_size, uint32_t value) { return value ? field_id_size + varint(value) : 0; } static constexpr uint32_t calc_uint32_force(uint32_t field_id_size, uint32_t value) { @@ -644,7 +644,7 @@ class ProtoSize { static constexpr uint32_t calc_uint64_force(uint32_t field_id_size, uint64_t value) { return field_id_size + varint(value); } - static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) { + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_length(uint32_t field_id_size, size_t len) { return len ? field_id_size + varint(static_cast(len)) + static_cast(len) : 0; } static constexpr uint32_t calc_length_force(uint32_t field_id_size, size_t len) {