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%)
- Combine tag byte + fixed32 value into single write_tag_and_fixed32()
method: pos[0] = tag, memcpy(pos+1, &value, 4), pos += 5
- Extract calculate_tag() from duplicated computation in
calculate_field_id_size() and encode_content
Instead of ALWAYS_INLINE on encode_field_raw (which bloated all
callers), have the code generator precompute the tag byte and emit
write_raw_byte(tag) + write_fixed32_raw(value) directly.
This gives the same tight codegen (single byte store + memcpy) for
key fields without inflating encode_bool/encode_uint32/etc.
+108 bytes flash vs baseline, -48 bytes vs the ALWAYS_INLINE approach.
- Add ESPHOME_ALWAYS_INLINE to encode_fixed32 so the compiler inlines it
on hot paths despite -Os heuristics (removing noinline alone was not
enough — gcc still chose not to inline at 51 call sites)
- Mark all fixed32 key fields in api.proto with [(force) = true] since
entity keys are FNV hashes and never zero, eliminating the zero-check
branch and making calculate_size() use constants
+96 bytes flash (555167 → 555263) on ESP32 — negligible for removing
branch + call overhead on every sensor state encode.
Profiling showed the noinline call overhead dominated hot paths like
SensorStateResponse encoding, where encode_fixed32 is called twice
per message (once for key, once via encode_float for state).
The function body is trivial (tag byte + 4-byte memcpy), so the
function call prologue/epilogue cost exceeded the actual work.
Despite 51 call sites, removing noinline shows no measurable flash
size increase (555167 bytes before and after on ESP32).