From 69d3f563d5e3176bea6015ea53ac332656422ceb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 15:43:36 -1000 Subject: [PATCH] [core] Replace std::vector in CallbackManager with trivial-copy container Replace std::vector> with a minimal container that exploits Callback's trivially-copyable nature. Since Callback is just {fn_ptr, ctx_ptr}, reallocation is a plain __builtin_memcpy instead of std::vector's exception-safe move/copy machinery. Uses uint16_t for size/capacity (8 bytes on 32-bit vs 12 for std::vector), and grows to exact size since callbacks are registered during setup() and most instances have 0-1 callbacks. Eliminates _M_realloc_insert/_M_default_append template instantiations per callback signature type. --- esphome/core/helpers.h | 72 +++++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 82c6b3833c..1557ca0790 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1785,30 +1785,78 @@ template struct Callback { 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() { 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 { + if (this != &other) { + delete[] this->data_; + this->data_ = other.data_; + this->size_ = other.size_; + this->capacity_ = other.capacity_; + other.data_ = nullptr; + other.size_ = 0; + other.capacity_ = 0; + } + 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...); } + void call(Ts... args) { + for (uint16_t i = 0; i < this->size_; i++) + this->data_[i].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_; + void add_(CbType cb) { + if (this->size_ == this->capacity_) { + auto *new_data = new CbType[this->size_ + 1]; + if (this->data_) { + __builtin_memcpy(new_data, this->data_, this->size_ * sizeof(CbType)); + delete[] this->data_; + } + this->data_ = new_data; + this->capacity_ = this->size_ + 1; + } + this->data_[this->size_++] = cb; + } + CbType *data_{nullptr}; + uint16_t size_{0}; + uint16_t capacity_{0}; }; template class LazyCallbackManager; @@ -1820,7 +1868,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.