From 96c398648178086ceaf9f28c99e37acfaed227c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Apr 2026 15:58:17 -1000 Subject: [PATCH] [core] Replace std::vector in CallbackManager with trivial-copy container (#15272) --- esphome/core/helpers.cpp | 13 ++++++++ esphome/core/helpers.h | 68 +++++++++++++++++++++++++++++++++------- 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 1732fc72e8..5940f6ec98 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -22,6 +22,19 @@ namespace esphome { static const char *const TAG = "helpers"; +__attribute__((noinline, cold)) void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity, + size_t elem_size) { + ESPHOME_DEBUG_ASSERT(size < UINT16_MAX); + uint16_t new_cap = size + 1; + auto *new_data = ::operator new(new_cap *elem_size); + if (data) { + __builtin_memcpy(new_data, data, size * elem_size); + ::operator delete(data); + } + capacity = new_cap; + return new_data; +} + static const uint16_t CRC16_A001_LE_LUT_L[] = {0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440}; static const uint16_t CRC16_A001_LE_LUT_H[] = {0x0000, 0xcc01, 0xd801, 0x1400, 0xf001, 0x3c00, 0x2800, 0xe401, diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index f96b888e28..c26bbe17b7 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1801,33 +1801,77 @@ template struct Callback { } }; +/// Grow a CallbackManager's backing array to exactly size+1. Defined in helpers.cpp. +void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity, size_t elem_size); + template class CallbackManager; /** Helper class to allow having multiple subscribers to a callback. + * + * Uses a trivial-copyable-specialized container instead of std::vector to avoid + * template bloat (_M_realloc_insert, exception-safe copies). Since Callback is + * trivially copyable (just {fn_ptr, ctx_ptr}), reallocation is a plain memcpy. + * Uses uint16_t for size/capacity (8 bytes on 32-bit vs 12 for std::vector). + * Grows to exact size on each add — callbacks are registered during setup() + * and most instances have only 1-2 callbacks, so slack capacity is wasteful. * * @tparam Ts The arguments for the callbacks, wrapped in void(). */ template class CallbackManager { + using CbType = Callback; + static_assert(std::is_trivially_copyable_v, "Callback must be trivially copyable"); + public: + CallbackManager() = default; + ~CallbackManager() { ::operator delete(this->data_); } + + // Non-copyable (would alias data_), movable (for std::map support) + CallbackManager(const CallbackManager &) = delete; + CallbackManager &operator=(const CallbackManager &) = delete; + CallbackManager(CallbackManager &&other) noexcept + : data_(other.data_), size_(other.size_), capacity_(other.capacity_) { + other.data_ = nullptr; + other.size_ = 0; + other.capacity_ = 0; + } + CallbackManager &operator=(CallbackManager &&other) noexcept { + std::swap(this->data_, other.data_); + std::swap(this->size_, other.size_); + std::swap(this->capacity_, other.capacity_); + return *this; + } + /// Add any callable. Small trivially-copyable callables (like [this] lambdas) /// are stored inline without heap allocation or std::function. - template void add(F &&callback) { this->add_(Callback::create(std::forward(callback))); } - - /// Call all callbacks in this manager. No null check on invoke. - void call(Ts... args) { - for (auto &cb : this->callbacks_) - cb.call(args...); - } - size_t size() const { return this->callbacks_.size(); } + template void add(F &&callback) { this->add_(CbType::create(std::forward(callback))); } /// Call all callbacks in this manager. - void operator()(Ts... args) { call(args...); } + inline void ESPHOME_ALWAYS_INLINE call(Ts... args) { + if (this->size_ != 0) { + for (auto *it = this->data_, *end = it + this->size_; it != end; ++it) { + it->call(args...); + } + } + } + uint16_t size() const { return this->size_; } + + /// Call all callbacks in this manager. + void operator()(Ts... args) { this->call(args...); } protected: template friend class LazyCallbackManager; /// Non-template core to avoid code duplication per lambda type. - void add_(Callback cb) { this->callbacks_.push_back(cb); } - std::vector> callbacks_; + /// Inline fast path; cold growth path is in helpers.cpp via callback_manager_grow(). + void add_(CbType cb) { + if (this->size_ == this->capacity_) { + this->data_ = + static_cast(callback_manager_grow(this->data_, this->size_, this->capacity_, sizeof(CbType))); + } + this->data_[this->size_++] = cb; + } + CbType *data_{nullptr}; + uint16_t size_{0}; + uint16_t capacity_{0}; }; /** CallbackManager backed by StaticVector for compile-time-known callback counts. @@ -1871,7 +1915,7 @@ template class LazyCallbackManager; * from API and web_server components). * * Memory overhead comparison (32-bit systems): - * - CallbackManager: 12 bytes (empty std::vector) + * - CallbackManager: 8 bytes (pointer + uint16 size + uint16 capacity) * - LazyCallbackManager: 4 bytes (nullptr pointer) * * Uses plain pointer instead of unique_ptr to avoid template instantiation overhead.