[api] Precompute tag bytes for forced varint and length-delimited fields

Extend the precomputed-tag approach from fixed32 key fields to all forced
fields with single-byte tags (field IDs 1-15). The code generator now
emits write_raw_byte(tag) followed by the raw encode primitive instead
of calling the full encode_* method.

For varint types (uint32, uint64, sint32, sint64, int64, bool, enum),
this eliminates the zero-check branch and encode_field_raw indirection.
For length-delimited types (bytes, string), it additionally skips the
encode_string wrapper.

Benchmarked on real hardware with BluetoothLERawAdvertisementsResponse
(12 advertisements per message, 10000 iterations):

ESP32 (Xtensa dual-core 240MHz):
  encode: 38498 -> 30460 ns/op (-20.9%)
  calc+encode: 48479 -> 40458 ns/op (-16.6%)

ESP32-C3 (RISC-V single-core 160MHz):
  encode: 54199 -> 40342 ns/op (-25.6%)
  calc+encode: 57800 -> 51365 ns/op (-11.1%)
This commit is contained in:
J. Nick Koston
2026-03-21 11:48:28 -10:00
parent d8d5619ba7
commit da926d904e
3 changed files with 93 additions and 3 deletions

View File

@@ -2249,10 +2249,14 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
return true;
}
void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const {
buffer.encode_uint64(1, this->address, true);
buffer.encode_sint32(2, this->rssi, true);
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_uint32(3, this->address_type);
buffer.encode_bytes(4, this->data, this->data_len, true);
buffer.write_raw_byte(34);
buffer.encode_varint_raw(this->data_len);
buffer.encode_raw(this->data, this->data_len);
}
uint32_t BluetoothLERawAdvertisement::calculate_size() const {
uint32_t size = 0;

View File

@@ -236,6 +236,17 @@ class ProtoWriteBuffer {
* Following https://protobuf.dev/programming-guides/encoding/#structure
*/
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); }
/// 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;
}
/// 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;
}
/// 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 {

View File

@@ -221,8 +221,58 @@ class TypeInfo(ABC):
decode_64bit = None
# Mapping from encode_func to raw encode expression template.
# When a forced field has a single-byte tag, the code generator emits
# write_raw_byte(tag) + raw encode instead of the full encode_* method,
# eliminating the zero-check branch and encode_field_raw indirection.
# {value} is replaced with the actual field expression.
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_sint64": "buffer.encode_varint_raw_64(encode_zigzag64({value}));",
"encode_int64": "buffer.encode_varint_raw_64(static_cast<uint64_t>({value}));",
"encode_bool": "buffer.write_raw_byte({value} ? 0x01 : 0x00);",
}
def _encode_with_precomputed_tag(self, value_expr: str) -> str | None:
"""Try to emit a precomputed-tag encode for a forced field.
Returns the raw encode string if the tag is a single byte and the
encode_func has a known raw equivalent, or None otherwise.
"""
if not self.force:
return None
tag = self.calculate_tag()
if tag >= 128:
return None
raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func)
if raw_expr is None:
return None
return f"buffer.write_raw_byte({tag});\n{raw_expr.format(value=value_expr)}"
def _encode_bytes_with_precomputed_tag(
self, data_expr: str, len_expr: str
) -> str | None:
"""Try to emit a precomputed-tag encode for a forced bytes/string field.
Returns the raw encode string if the tag is a single byte, or None.
"""
if not self.force:
return None
tag = self.calculate_tag()
if tag >= 128:
return None
return (
f"buffer.write_raw_byte({tag});\n"
f"buffer.encode_varint_raw({len_expr});\n"
f"buffer.encode_raw({data_expr}, {len_expr});"
)
@property
def encode_content(self) -> str:
if result := self._encode_with_precomputed_tag(f"this->{self.field_name}"):
return result
if self.force:
return f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, true);"
return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});"
@@ -635,6 +685,11 @@ class StringType(TypeInfo):
@property
def encode_content(self) -> str:
# Use the StringRef
if result := self._encode_bytes_with_precomputed_tag(
f"this->{self.field_name}_ref_.c_str()",
f"this->{self.field_name}_ref_.size()",
):
return result
if self.force:
return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_, true);"
return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);"
@@ -801,6 +856,10 @@ class BytesType(TypeInfo):
@property
def encode_content(self) -> str:
if result := self._encode_bytes_with_precomputed_tag(
f"this->{self.field_name}_ptr_", f"this->{self.field_name}_len_"
):
return result
if self.force:
return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_, true);"
return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);"
@@ -908,6 +967,10 @@ class PointerToBytesBufferType(PointerToBufferTypeBase):
@property
def encode_content(self) -> str:
if result := self._encode_bytes_with_precomputed_tag(
f"this->{self.field_name}", f"this->{self.field_name}_len"
):
return result
if self.force:
return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);"
return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);"
@@ -957,6 +1020,10 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
@property
def encode_content(self) -> str:
if result := self._encode_bytes_with_precomputed_tag(
f"this->{self.field_name}.c_str()", f"this->{self.field_name}.size()"
):
return result
if self.force:
return (
f"buffer.encode_string({self.number}, this->{self.field_name}, true);"
@@ -1124,6 +1191,10 @@ class FixedArrayBytesType(TypeInfo):
@property
def encode_content(self) -> str:
if result := self._encode_bytes_with_precomputed_tag(
f"this->{self.field_name}", f"this->{self.field_name}_len"
):
return result
if self.force:
return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);"
return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);"
@@ -1199,6 +1270,10 @@ class EnumType(TypeInfo):
@property
def encode_content(self) -> str:
if result := self._encode_with_precomputed_tag(
f"static_cast<uint32_t>(this->{self.field_name})"
):
return result
if self.force:
return f"buffer.{self.encode_func}({self.number}, static_cast<uint32_t>(this->{self.field_name}), true);"
return f"buffer.{self.encode_func}({self.number}, static_cast<uint32_t>(this->{self.field_name}));"