Make micros_to_millis a template on return type (default uint32_t).
millis_64() now calls micros_to_millis<uint64_t>() to eliminate
__udivdi3 there too — same Euclidean decomposition, just with a
32x32->64 multiply for hi*Q instead of truncating.
Add 64-bit variant tests (140 total, all passing).
Add Python unit tests (70 cases) verifying the Euclidean decomposition
matches us // 1000 across edge cases: small values, hi/lo boundaries,
carry-path overflow, realistic uptimes, and shift-boundary mod-8
variations.
Make micros_to_millis constexpr to enable compile-time validation
via static_assert.
On 32-bit targets GCC does not optimize 64-bit constant division
into a multiply-by-reciprocal, emitting a call to __udivdi3 instead
(~650-710 ns on Xtensa @ 240 MHz).
Add micros_to_millis() which exploits 1000 = 8 * 125: a free
right-shift by 3 followed by Euclidean decomposition with D=125,
reducing the 64-bit division to a single 32-bit / 125U that GCC
compiles to a multiply-by-reciprocal.
Benchmarked at 258 ns per call on ESP32 classic — a 2.5-2.8x
speedup. With ~21 millis() calls per loop iteration this saves
~9 us per loop.
Factor 1000 as 8 * 125. The divide-by-8 is a free right-shift,
then Euclidean decomposition with D=125 has a smaller remainder
(R=46 vs R=296), making hi*R cheaper and the safe input range
51x wider (~3.2e18 vs ~6.2e16 microseconds).
Benchmarked ~7% faster than the divide-by-1000 variant on ESP32
classic (257 ns vs 277 ns per call at 240 MHz).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On 32-bit targets, GCC does not optimize 64-bit constant division
into a multiply-by-reciprocal and instead calls __udivdi3 (software
64-bit divide). This replaces the division with Euclidean
decomposition using a single 32-bit divide that GCC optimizes into
a multiply-by-reciprocal.
Benchmarked on ESP32 classic (Xtensa LX6, 240 MHz):
__udivdi3: 628-705 ns
fast_div1000_32: 269-279 ns (2.3-2.6x faster)
millis() is called ~21 times per main loop iteration. At ~350 ns
savings per call, this saves ~7 us per loop iteration.
Cache struct lwip_sock* pointers at socket registration time instead of
looking them up by fd on every iteration. The previous code stored file
descriptors and called lwip_socket_dbg_get_socket(fd) on every loop
iteration to convert each fd back to a pointer — only to read a single
field from it.
- register_socket(lwip_sock*) returns bool and hooks the callback
internally — replaces the separate register_socket_fd + hook dance
- Hot loop iterates struct lwip_sock* directly instead of converting
fds each iteration
- Socket::ready() uses the cached pointer instead of fd lookup
- esphome_lwip_hook_socket() takes struct lwip_sock* directly
- esphome_lwip_socket_has_data() moved to header as static inline
for inlining — uses offset-based access verified by _Static_assert
- Null cached_sock_ on close to prevent post-close dereference
- socket_ready() tolerant of null pointer for safety
Cache struct lwip_sock* pointers at socket registration time instead of
looking them up by fd on every iteration. The previous code stored file
descriptors and called lwip_socket_dbg_get_socket(fd) on every loop
iteration to convert each fd back to a pointer — only to read a single
field from it.
- register_socket(lwip_sock*) returns bool and hooks the callback
internally — replaces the separate register_socket_fd + hook dance
- Hot loop iterates struct lwip_sock* directly instead of converting
fds each iteration
- Socket::ready() uses the cached pointer instead of fd lookup
- esphome_lwip_hook_socket() takes struct lwip_sock* directly
- esphome_lwip_socket_has_data() moved to header as static inline
for inlining — uses offset-based access verified by _Static_assert
- Null cached_sock_ on close to prevent post-close dereference
- socket_ready() tolerant of null pointer for safety
Cache struct lwip_sock* pointers at socket registration time instead of
looking them up by fd on every iteration. The previous code stored file
descriptors and called lwip_socket_dbg_get_socket(fd) on every loop
iteration to convert each fd back to a pointer — only to read a single
field from it.
- register_socket(lwip_sock*) returns bool and hooks the callback
internally — replaces the separate register_socket_fd + hook dance
- Hot loop iterates struct lwip_sock* directly instead of converting
fds each iteration
- Socket::ready() uses the cached pointer instead of fd lookup
- esphome_lwip_hook_socket() takes struct lwip_sock* directly
- esphome_lwip_socket_has_data() moved to header as static inline
for inlining — uses offset-based access verified by _Static_assert
- Null cached_sock_ on close to prevent post-close dereference
- socket_ready() tolerant of null pointer for safety
Cache struct lwip_sock* pointers at socket registration time instead of
looking them up by fd on every iteration. The previous code stored file
descriptors and called lwip_socket_dbg_get_socket(fd) on every loop
iteration to convert each fd back to a pointer — only to read a single
field from it.
- register_socket(lwip_sock*) returns bool and hooks the callback
internally — replaces the separate register_socket_fd + hook dance
- Hot loop iterates struct lwip_sock* directly instead of converting
fds each iteration
- Socket::ready() uses the cached pointer instead of fd lookup
- esphome_lwip_hook_socket() takes struct lwip_sock* directly
- esphome_lwip_socket_has_data() moved to header as static inline
for inlining — uses offset-based access verified by _Static_assert
- Null cached_sock_ on close to prevent post-close dereference
- socket_ready() tolerant of null pointer for safety