[core] Eliminate __udivdi3 in millis() on ESP32 and RP2040

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.
This commit is contained in:
J. Nick Koston
2026-03-01 23:49:18 -10:00
parent 3160457ca6
commit 0dc56d7a4d
3 changed files with 35 additions and 2 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ extern "C" __attribute__((weak)) void initArduino() {}
namespace esphome {
void HOT yield() { vPortYield(); }
uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); }
uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time())); }
uint64_t HOT millis_64() { return static_cast<uint64_t>(esp_timer_get_time()) / 1000ULL; }
void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); }
+1 -1
View File
@@ -12,7 +12,7 @@ namespace esphome {
void HOT yield() { ::yield(); }
uint64_t millis_64() { return time_us_64() / 1000ULL; }
uint32_t HOT millis() { return static_cast<uint32_t>(millis_64()); }
uint32_t HOT millis() { return micros_to_millis(time_us_64()); }
void HOT delay(uint32_t ms) { ::delay(ms); }
uint32_t HOT micros() { return ::micros(); }
void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
+33
View File
@@ -599,6 +599,39 @@ template<std::integral T> constexpr uint32_t fnv1a_hash_extend(uint32_t hash, T
constexpr uint32_t fnv1a_hash(const char *str) { return fnv1a_hash_extend(FNV1_OFFSET_BASIS, str); }
inline uint32_t fnv1a_hash(const std::string &str) { return fnv1a_hash(str.c_str()); }
/// Convert a 64-bit microsecond count to a 32-bit millisecond count without
/// calling __udivdi3 (software 64-bit divide, ~1200 ns on Xtensa @ 240 MHz).
///
/// On 32-bit targets, GCC does not optimize 64-bit constant division into a
/// multiply-by-reciprocal. Since 1000 = 8 * 125, we first right-shift by 3
/// (free divide-by-8), then use the Euclidean division identity to decompose
/// the remaining 64-bit divide-by-125 into a single 32-bit division:
///
/// floor(us / 1000) = floor(floor(us / 8) / 125) [exact for integers]
/// 2^32 = Q * 125 + R (34359738 * 125 + 46)
/// (hi * 2^32 + lo) / 125 = hi * Q + (hi * R + lo) / 125
///
/// GCC optimizes the remaining 32-bit "/ 125U" into a multiply-by-reciprocal
/// (mulhu + shift), so no division instruction is emitted.
///
/// Safe for us up to ~3.2e18 (~101,700 years of microseconds).
///
/// See: https://en.wikipedia.org/wiki/Euclidean_division
/// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html
inline ESPHOME_ALWAYS_INLINE uint32_t micros_to_millis(uint64_t us) {
static constexpr uint32_t D = 125U;
static constexpr uint32_t Q = static_cast<uint32_t>((1ULL << 32) / D); // 34359738
static constexpr uint32_t R = static_cast<uint32_t>((1ULL << 32) % D); // 46
// 1000 = 8 * 125; divide-by-8 is a free shift
uint64_t x = us >> 3;
uint32_t lo = static_cast<uint32_t>(x);
uint32_t hi = static_cast<uint32_t>(x >> 32);
// Combine remainder term: hi * (2^32 % 125) + lo
uint32_t adj = hi * R + lo;
// If adj overflowed, the true value is 2^32 + adj; apply the identity again
return hi * Q + (adj < lo ? (adj + R) / D + Q : adj / D);
}
/// Return a random 32-bit unsigned integer.
uint32_t random_uint32();
/// Return a random float between 0 and 1.