[scheduler] Use relaxed memory ordering for atomic reads under lock

When the scheduler lock is already held, atomic loads of the remove
flag don't need acquire ordering — the mutex provides all necessary
synchronization guarantees. Add is_item_removed_locked_() helper that
uses memory_order_relaxed and use it in all _locked_ call sites.

This eliminates redundant memw barriers on Xtensa, saving 12 bytes
in matches_item_locked_ (152 → 140 B) and reducing pipeline stalls
in the scheduler hot path.
This commit is contained in:
J. Nick Koston
2026-02-19 23:10:39 -06:00
parent efe54e3b5e
commit 25b979bbed
2 changed files with 18 additions and 6 deletions

View File

@@ -406,7 +406,7 @@ void Scheduler::full_cleanup_removed_items_() {
// Compact in-place: move valid items forward, recycle removed ones
size_t write = 0;
for (size_t read = 0; read < this->items_.size(); ++read) {
if (!is_item_removed_(this->items_[read].get())) {
if (!is_item_removed_locked_(this->items_[read].get())) {
if (write != read) {
this->items_[write] = std::move(this->items_[read]);
}
@@ -508,7 +508,7 @@ void HOT Scheduler::call(uint32_t now) {
// Multi-threaded platforms without atomics: must take lock to safely read remove flag
{
LockGuard guard{this->lock_};
if (is_item_removed_(item.get())) {
if (is_item_removed_locked_(item.get())) {
this->recycle_item_main_loop_(this->pop_raw_locked_());
this->to_remove_--;
continue;
@@ -572,7 +572,7 @@ void HOT Scheduler::call(uint32_t now) {
void HOT Scheduler::process_to_add() {
LockGuard guard{this->lock_};
for (auto &it : this->to_add_) {
if (is_item_removed_(it.get())) {
if (is_item_removed_locked_(it.get())) {
// Recycle cancelled items
this->recycle_item_main_loop_(std::move(it));
continue;

View File

@@ -311,8 +311,8 @@ class Scheduler {
// Fixes: https://github.com/esphome/esphome/issues/11940
if (!item)
return false;
if (item->component != component || item->type != type || (skip_removed && item->remove) ||
(match_retry && !item->is_retry)) {
if (item->component != component || item->type != type ||
(skip_removed && this->is_item_removed_locked_(item.get())) || (match_retry && !item->is_retry)) {
return false;
}
// Name type must match
@@ -465,6 +465,18 @@ class Scheduler {
#endif
}
// Helper to check if item is marked for removal when lock is already held.
// Uses relaxed ordering since the mutex provides all necessary synchronization.
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
bool is_item_removed_locked_(SchedulerItem *item) const {
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// Lock already held - relaxed is sufficient, mutex provides ordering
return item->remove.load(std::memory_order_relaxed);
#else
return item->remove;
#endif
}
// Helper to set item removal flag (platform-specific)
// For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
// function. Uses memory_order_release when setting to true (for cancellation synchronization),
@@ -521,7 +533,7 @@ class Scheduler {
// it will iterate over these nullptr items. This check prevents crashes.
if (!item)
continue;
if (is_item_removed_(item.get()) &&
if (this->is_item_removed_locked_(item.get()) &&
this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT,
match_retry, /* skip_removed= */ false)) {
return true;