From 36881166a8868676a48cc23350cfb9916b9cee99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Apr 2026 16:16:44 -1000 Subject: [PATCH] [light] Replace soft-float range check with union bit-cast + unsigned compare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inlined `value < 0.0f || value > 1.0f` check in the clamp loop costs ~50 B per iteration on ESP8266 (two libgcc soft-float calls with register spills). IEEE 754 floats in [0.0f, 1.0f] have bit patterns in [0x00000000, 0x3F800000]; anything out of range — values > 1.0f, negatives (sign bit set → huge unsigned interpretation), NaN, Infinity — has a strictly larger unsigned interpretation. A single `pun.u > 0x3F800000u` covers every case. Using a union for the type-pun rather than memcpy/bit_cast because those don't optimize to a no-op on xtensa-gcc (same reason api/proto.h's float_to_raw() uses a union). The loop body is now two instructions for the range check: l32i a2, a9, 0 ; load raw u32 bgeu a10, a2, ... ; compare against pre-hoisted 1.0f bits Size delta vs. the prior float-compare form: ESP32-IDF: validate_ -16 B, net -12 B ESP8266: validate_ -20 B, net -20 B vs. dev baseline (isolated light build, matched funcs): ESP32-IDF: -103 B code, +32 B PROGMEM table = -71 B net ESP8266: -42 B code, +32 B PROGMEM table = -10 B net --- esphome/components/light/light_call.cpp | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index df65883ed66..045a4f29356 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -334,8 +334,16 @@ LightColorValues LightCall::validate_() { // by bit position directly. Iterate only the set bits via __builtin_ctz + // clear-lowest-bit: HA can drive high-frequency automations through // perform(), so the hot path runs in O(popcount) instead of always - // scanning all eight slots. The range check is inlined here (cold path - // is the out-of-line helper) so an in-range value skips the call entirely. + // scanning all eight slots. + // + // The range check is done on the IEEE 754 bit pattern as an unsigned int, + // not on the float itself. Values in [0.0f, 1.0f] have bits in + // [0x00000000, 0x3F800000]; anything greater (as unsigned) is out of range: + // values > 1.0f have a larger bit pattern, and negative values have the + // sign bit (0x80000000) set which makes their unsigned interpretation + // enormous. One unsigned compare replaces two soft-float __ltsf2/__gtsf2 + // calls on ESP8266 and is essentially free on targets with an FPU too. + constexpr uint32_t ONE_F_BITS = 0x3F800000u; // bit pattern of 1.0f float *const src_fields = &this->brightness_; float *const dst_fields = &v.brightness_; unsigned active = this->flags_ & CLAMP_FLAGS_MASK; @@ -343,7 +351,14 @@ LightColorValues LightCall::validate_() { unsigned bit = __builtin_ctz(active); active &= active - 1; // clear lowest set bit float &value = src_fields[bit]; - if (value < 0.0f || value > 1.0f) + // Union type-pun (GCC/Clang extension): bit_cast/memcpy don't optimize to + // a no-op on xtensa-gcc, same reasoning as api/proto.h float_to_raw(). + union { + float f; + uint32_t u; + } pun; + pun.f = value; + if (pun.u > ONE_F_BITS) log_out_of_range_and_clamp_(name, value, &FIELD_NAMES[bit], 0.0f, 1.0f); dst_fields[bit] = value; }