From 25baec48ee1d94eb4ef6ede335d0f3013f5e38fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 11:02:03 -0600 Subject: [PATCH] [api] Optimize varint encoding to eliminate uint64_t widening Replace ProtoVarInt temporaries with direct uint32_t encoding functions to avoid unnecessary 64-bit widening on 32-bit architectures (ESP32 Xtensa). - Add encode_varint_to_buffer() free function for direct buffer encoding - Replace encode_varint_raw(ProtoVarInt) with uint32_t-native implementation - Add encode_varint_raw_64() for the few call sites needing 64-bit (BLE) - Remove unused ProtoVarInt::encode() and encode_to_buffer_unchecked() - Remove dead null checks from ProtoVarInt::parse() Net savings: -152 bytes flash on ESP32 IDF. --- .../api/api_frame_helper_plaintext.cpp | 5 +- esphome/components/api/proto.h | 92 +++++++------------ 2 files changed, 36 insertions(+), 61 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index ed3cc8934e..5069dbf68b 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -295,9 +295,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe buf_start[header_offset] = 0x00; // indicator // Encode varints directly into buffer - ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); - ProtoVarInt(msg.message_type) - .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); + encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1); + encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len); // Add iovec for this message (header + payload) size_t msg_len = static_cast(total_header_len + msg.payload_size); diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 41ea0043f9..6eb4135df9 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -57,6 +57,16 @@ inline uint16_t count_packed_varints(const uint8_t *data, size_t len) { return count; } +/// Encode a varint directly into a pre-allocated buffer. +/// Caller must ensure buffer has space (use ProtoSize::varint() to calculate). +inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) { + while (val > 0x7F) { + *buffer++ = static_cast(val | 0x80); + val >>= 7; + } + *buffer = static_cast(val); +} + /* * StringRef Ownership Model for API Protocol Messages * =================================================== @@ -93,17 +103,14 @@ class ProtoVarInt { ProtoVarInt() : value_(0) {} explicit ProtoVarInt(uint64_t value) : value_(value) {} + /// Parse a varint from buffer. consumed must be a valid pointer (not null). static optional parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) { - if (len == 0) { - if (consumed != nullptr) - *consumed = 0; + if (len == 0) return {}; - } // Most common case: single-byte varint (values 0-127) if ((buffer[0] & 0x80) == 0) { - if (consumed != nullptr) - *consumed = 1; + *consumed = 1; return ProtoVarInt(buffer[0]); } @@ -122,14 +129,11 @@ class ProtoVarInt { result |= uint64_t(val & 0x7F) << uint64_t(bitpos); bitpos += 7; if ((val & 0x80) == 0) { - if (consumed != nullptr) - *consumed = i + 1; + *consumed = i + 1; return ProtoVarInt(result); } } - if (consumed != nullptr) - *consumed = 0; return {}; // Incomplete or invalid varint } @@ -153,50 +157,6 @@ class ProtoVarInt { // with ZigZag encoding return decode_zigzag64(this->value_); } - /** - * Encode the varint value to a pre-allocated buffer without bounds checking. - * - * @param buffer The pre-allocated buffer to write the encoded varint to - * @param len The size of the buffer in bytes - * - * @note The caller is responsible for ensuring the buffer is large enough - * to hold the encoded value. Use ProtoSize::varint() to calculate - * the exact size needed before calling this method. - * @note No bounds checking is performed for performance reasons. - */ - void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) { - uint64_t val = this->value_; - if (val <= 0x7F) { - buffer[0] = val; - return; - } - size_t i = 0; - while (val && i < len) { - uint8_t temp = val & 0x7F; - val >>= 7; - if (val) { - buffer[i++] = temp | 0x80; - } else { - buffer[i++] = temp; - } - } - } - void encode(std::vector &out) { - uint64_t val = this->value_; - if (val <= 0x7F) { - out.push_back(val); - return; - } - while (val) { - uint8_t temp = val & 0x7F; - val >>= 7; - if (val) { - out.push_back(temp | 0x80); - } else { - out.push_back(temp); - } - } - } protected: uint64_t value_; @@ -256,8 +216,24 @@ class ProtoWriteBuffer { public: ProtoWriteBuffer(std::vector *buffer) : buffer_(buffer) {} void write(uint8_t value) { this->buffer_->push_back(value); } - void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); } - void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); } + void encode_varint_raw(uint32_t value) { + if (value <= 0x7F) { + this->buffer_->push_back(static_cast(value)); + return; + } + while (value) { + uint8_t temp = value & 0x7F; + value >>= 7; + this->buffer_->push_back(value ? (temp | 0x80) : temp); + } + } + void encode_varint_raw_64(uint64_t value) { + while (value > 0x7F) { + this->buffer_->push_back(static_cast(value | 0x80)); + value >>= 7; + } + this->buffer_->push_back(static_cast(value)); + } /** * Encode a field key (tag/wire type combination). * @@ -307,7 +283,7 @@ class ProtoWriteBuffer { if (value == 0 && !force) return; this->encode_field_raw(field_id, 0); // type 0: Varint - uint64 - this->encode_varint_raw(ProtoVarInt(value)); + this->encode_varint_raw_64(value); } void encode_bool(uint32_t field_id, bool value, bool force = false) { if (!value && !force) @@ -938,7 +914,7 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa this->buffer_->resize(this->buffer_->size() + varint_length_bytes); // Write the length varint directly - ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes); + encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin); // Now encode the message content - it will append to the buffer value.encode(*this);