diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index ae2cd2bae8d..9515b9c3b6f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2252,7 +2252,7 @@ void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { buffer.write_raw_byte(8); buffer.encode_varint_raw_64(this->address); buffer.write_raw_byte(16); - buffer.encode_varint_raw(encode_zigzag32(this->rssi)); + buffer.encode_varint_raw_short(encode_zigzag32(this->rssi)); buffer.encode_uint32(3, this->address_type); buffer.write_raw_byte(34); buffer.encode_varint_raw(this->data_len); diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 4f5b3f0918f..dde7f0b32d2 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -11,13 +11,17 @@ static const char *const TAG = "api.proto"; uint32_t ProtoSize::varint_slow(uint32_t value) { return varint_wide(value); } void ProtoWriteBuffer::encode_varint_raw_slow_(uint32_t value) { + // Use __restrict__ so the compiler knows pos doesn't alias this-> + // and can keep it in a register across the loop. + uint8_t *__restrict__ pos = this->pos_; do { - this->debug_check_bounds_(1); - *this->pos_++ = static_cast(value | 0x80); + this->sync_debug_check_bounds_(pos, 1); + *pos++ = static_cast(value | 0x80); value >>= 7; } while (value > 0x7F); - this->debug_check_bounds_(1); - *this->pos_++ = static_cast(value); + this->sync_debug_check_bounds_(pos, 1); + *pos++ = static_cast(value); + this->pos_ = pos; } ProtoVarIntResult ProtoVarInt::parse_slow(const uint8_t *buffer, uint32_t len) { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index b629018a919..9db03d62895 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -202,19 +202,45 @@ class ProtoWriteBuffer { inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) { if (value < 128) [[likely]] { this->debug_check_bounds_(1); - *this->pos_++ = static_cast(value); + uint8_t *__restrict__ pos = this->pos_; + *pos++ = static_cast(value); + this->pos_ = pos; + return; + } + this->encode_varint_raw_slow_(value); + } + /// Encode a varint that is expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths). + /// Inlines both the 1-byte and 2-byte paths; falls back to slow path for 3+ bytes. + inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_short(uint32_t value) { + if (value < 128) [[likely]] { + this->debug_check_bounds_(1); + uint8_t *__restrict__ pos = this->pos_; + *pos++ = static_cast(value); + this->pos_ = pos; + return; + } + if (value < 16384) [[likely]] { + this->debug_check_bounds_(2); + uint8_t *__restrict__ pos = this->pos_; + *pos++ = static_cast(value | 0x80); + *pos++ = static_cast(value >> 7); + this->pos_ = pos; return; } this->encode_varint_raw_slow_(value); } void encode_varint_raw_64(uint64_t value) { + // Use __restrict__ so the compiler knows pos doesn't alias this-> + // and can keep it in a register across the loop. + uint8_t *__restrict__ pos = this->pos_; while (value > 0x7F) { - this->debug_check_bounds_(1); - *this->pos_++ = static_cast(value | 0x80); + this->sync_debug_check_bounds_(pos, 1); + *pos++ = static_cast(value | 0x80); value >>= 7; } - this->debug_check_bounds_(1); - *this->pos_++ = static_cast(value); + this->sync_debug_check_bounds_(pos, 1); + *pos++ = static_cast(value); + this->pos_ = pos; } /** * Encode a field key (tag/wire type combination). @@ -232,40 +258,52 @@ class ProtoWriteBuffer { /// Write a single precomputed tag byte. Tag must be < 128. inline void write_raw_byte(uint8_t b) ESPHOME_ALWAYS_INLINE { this->debug_check_bounds_(1); - *this->pos_++ = b; + uint8_t *__restrict__ pos = this->pos_; + *pos++ = b; + this->pos_ = pos; } /// Write raw bytes to the buffer (no tag, no length prefix). inline void encode_raw(const void *data, size_t len) ESPHOME_ALWAYS_INLINE { this->debug_check_bounds_(len); - std::memcpy(this->pos_, data, len); - this->pos_ += len; + uint8_t *__restrict__ pos = this->pos_; + std::memcpy(pos, data, len); + this->pos_ = pos + len; } /// Write a precomputed tag byte + 32-bit value in one operation. /// Tag must be a single-byte varint (< 128). No zero check. inline void write_tag_and_fixed32(uint8_t tag, uint32_t value) ESPHOME_ALWAYS_INLINE { this->debug_check_bounds_(5); - this->pos_[0] = tag; + uint8_t *__restrict__ pos = this->pos_; + pos[0] = tag; #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ - std::memcpy(this->pos_ + 1, &value, 4); + std::memcpy(pos + 1, &value, 4); #else - this->pos_[1] = static_cast(value & 0xFF); - this->pos_[2] = static_cast((value >> 8) & 0xFF); - this->pos_[3] = static_cast((value >> 16) & 0xFF); - this->pos_[4] = static_cast((value >> 24) & 0xFF); + pos[1] = static_cast(value & 0xFF); + pos[2] = static_cast((value >> 8) & 0xFF); + pos[3] = static_cast((value >> 16) & 0xFF); + pos[4] = static_cast((value >> 24) & 0xFF); #endif - this->pos_ += 5; + this->pos_ = pos + 5; } void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { if (len == 0 && !force) return; this->encode_field_raw(field_id, 2); // type 2: Length-delimited string - this->encode_varint_raw(len); - // Direct memcpy into pre-sized buffer — avoids push_back() per-byte capacity checks - // and vector::insert() iterator overhead. ~10-11x faster for 16-32 byte strings. - this->debug_check_bounds_(len); - std::memcpy(this->pos_, string, len); - this->pos_ += len; + // Inline the length varint + memcpy under a single __restrict__ pos + // to avoid a store-load pair between encode_varint_raw and encode_raw. + uint8_t *__restrict__ pos = this->pos_; + if (len < 128) [[likely]] { + this->debug_check_bounds_(1 + len); + *pos++ = static_cast(len); + } else { + // Length >= 128: use slow path for the length varint, then re-hoist pos + this->encode_varint_raw_slow_(len); + pos = this->pos_; + this->debug_check_bounds_(len); + } + std::memcpy(pos, string, len); + this->pos_ = pos + len; } void encode_string(uint32_t field_id, const std::string &value, bool force = false) { this->encode_string(field_id, value.data(), value.size(), force); @@ -293,7 +331,9 @@ class ProtoWriteBuffer { return; this->encode_field_raw(field_id, 0); // type 0: Varint - bool this->debug_check_bounds_(1); - *this->pos_++ = value ? 0x01 : 0x00; + uint8_t *__restrict__ pos = this->pos_; + *pos++ = value ? 0x01 : 0x00; + this->pos_ = pos; } void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { if (value == 0 && !force) @@ -301,15 +341,17 @@ class ProtoWriteBuffer { this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32 this->debug_check_bounds_(4); + uint8_t *__restrict__ pos = this->pos_; #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ // Protobuf fixed32 is little-endian, so direct copy works - std::memcpy(this->pos_, &value, 4); - this->pos_ += 4; + std::memcpy(pos, &value, 4); + this->pos_ = pos + 4; #else - *this->pos_++ = (value >> 0) & 0xFF; - *this->pos_++ = (value >> 8) & 0xFF; - *this->pos_++ = (value >> 16) & 0xFF; - *this->pos_++ = (value >> 24) & 0xFF; + *pos++ = (value >> 0) & 0xFF; + *pos++ = (value >> 8) & 0xFF; + *pos++ = (value >> 16) & 0xFF; + *pos++ = (value >> 24) & 0xFF; + this->pos_ = pos; #endif } // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally @@ -366,9 +408,16 @@ class ProtoWriteBuffer { #ifdef ESPHOME_DEBUG_API void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION()); + /// Sync pos_ from a local pointer, then check bounds. For use in __restrict__ loops + /// where pos_ is hoisted into a local but debug checks need the current position. + void sync_debug_check_bounds_(uint8_t *pos, size_t bytes, const char *caller = __builtin_FUNCTION()) { + this->pos_ = pos; + this->debug_check_bounds_(bytes, caller); + } void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual); #else void debug_check_bounds_([[maybe_unused]] size_t bytes) {} + void sync_debug_check_bounds_(uint8_t *pos, [[maybe_unused]] size_t bytes) {} #endif APIBuffer *buffer_; @@ -531,6 +580,17 @@ class ProtoSize { return varint_wide(value); return varint_slow(value); } + /// Size of a varint expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths). + /// Inlines both checks; falls back to slow path for 3+ bytes. + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_short(uint32_t value) { + if (value < VARINT_THRESHOLD_1_BYTE) [[likely]] + return 1; + if (value < VARINT_THRESHOLD_2_BYTE) [[likely]] + return 2; + if (__builtin_is_constant_evaluated()) + return varint_wide(value); + return varint_slow(value); + } private: // Slow path for varint >= 128, outlined to keep fast path small @@ -645,10 +705,10 @@ class ProtoSize { return value ? field_id_size + 4 : 0; } static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) { - return value ? field_id_size + varint(encode_zigzag32(value)) : 0; + return value ? field_id_size + varint_short(encode_zigzag32(value)) : 0; } static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_sint32_force(uint32_t field_id_size, int32_t value) { - return field_id_size + varint(encode_zigzag32(value)); + return field_id_size + varint_short(encode_zigzag32(value)); } static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) { return value ? field_id_size + varint(value) : 0; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index f2a11141af0..811e7d12d75 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -229,7 +229,7 @@ class TypeInfo(ABC): RAW_ENCODE_MAP: dict[str, str] = { "encode_uint32": "buffer.encode_varint_raw({value});", "encode_uint64": "buffer.encode_varint_raw_64({value});", - "encode_sint32": "buffer.encode_varint_raw(encode_zigzag32({value}));", + "encode_sint32": "buffer.encode_varint_raw_short(encode_zigzag32({value}));", "encode_sint64": "buffer.encode_varint_raw_64(encode_zigzag64({value}));", "encode_int64": "buffer.encode_varint_raw_64(static_cast({value}));", "encode_bool": "buffer.write_raw_byte({value} ? 0x01 : 0x00);",