Improve futex_wake behaviour.
The current code yields and schedules a new thread if any equal or
higher-priority thread has become runnable.
The new code decomposes these cases. If a new thread is runnable and
should run now, the current thread yields. If a new thread is runnable
but is the same priority as the current thread, the scheduler ensures
that a timer interrupt is ready but does not yield immediately.
This improves the test suite performance by around 2% and a
producer-consumer microbenchmark by about 35%.
diff --git a/sdk/core/scheduler/main.cc b/sdk/core/scheduler/main.cc
index 0aef309..214bca0 100644
--- a/sdk/core/scheduler/main.cc
+++ b/sdk/core/scheduler/main.cc
@@ -14,7 +14,6 @@
#include <futex.h>
#include <interrupt.h>
#include <locks.hh>
-#include <new>
#include <priv/riscv.h>
#include <riscvreg.h>
#include <simulator.h>
@@ -122,6 +121,26 @@
*/
static constexpr auto UnboundedSleep = std::numeric_limits<uint32_t>::max();
+ enum FutexWakeKind
+ {
+ /**
+ * The futex wake did not make any threads runnable that would be
+ * scheduled preemptively.
+ */
+ NoYield,
+ /**
+ * The futex wake made a thread at the current priority level runnable,
+ * the caller should ensure that there is a timer interrupt scheduled
+ * to make the current thread yield later.
+ */
+ YieldLater,
+ /**
+ * The futex wake made a thread at a higher priority level runnable,
+ * the caller should yield to allow the other thread to run immediately.
+ */
+ YieldNow,
+ };
+
/**
* Helper that wakes a set of up to `count` threads waiting on the futex
* whose address is given by the `key` parameter.
@@ -134,11 +153,10 @@
* dropped back to the previous priority.
* - The number of sleeper that were awoken.
*/
- std::tuple<bool, bool, int>
+ std::tuple<bool, int>
futex_wake(ptraddr_t key,
uint32_t count = std::numeric_limits<uint32_t>::max())
{
- bool shouldYield = false;
bool shouldRecalculatePriorityBoost = false;
// The number of threads that we've woken, this is the return value on
// success.
@@ -150,7 +168,8 @@
{
shouldRecalculatePriorityBoost |=
thread->futexPriorityInheriting;
- shouldYield = thread->ready(Thread::WakeReason::Futex);
+ thread->ready(Thread::WakeReason::Futex);
+ Debug::log("futex_wake woke thread {}", thread->id_get());
count--;
woke++;
}
@@ -163,9 +182,10 @@
MultiWaiterInternal::wake_waiters(key, count);
count -= multiwaitersWoken;
woke += multiwaitersWoken;
- shouldYield |= (multiwaitersWoken > 0);
}
- return {shouldYield, shouldRecalculatePriorityBoost, woke};
+ Debug::log("futex_wake on {} woke {} waiters", key, woke);
+
+ return {shouldRecalculatePriorityBoost, woke};
}
} // namespace
@@ -297,8 +317,11 @@
word++;
// Wake anyone sleeping on this futex. Interrupt futexes
// are not priority inheriting.
- std::tie(schedNeeded, std::ignore, std::ignore) =
+ int woke;
+ Debug::log("Waking waiters on interrupt futex {}", &word);
+ std::tie(std::ignore, woke) =
futex_wake(Capability{&word}.address());
+ schedNeeded |= (woke > 0);
});
tick = schedNeeded;
break;
@@ -530,7 +553,24 @@
}
ptraddr_t key = Capability{address}.address();
- auto [shouldYield, shouldResetPrioirity, woke] = futex_wake(key, count);
+ auto [shouldResetPrioirity, woke] = futex_wake(key, count);
+
+ FutexWakeKind shouldYield = NoYield;
+
+ if (woke > 0)
+ {
+ auto *thread = Thread::current_get();
+ if (!thread->is_highest_priority())
+ {
+ shouldYield = YieldNow;
+ }
+ else if (thread->has_priority_peers())
+ {
+ shouldYield =
+ thread->has_run_for_full_tick() ? YieldNow : YieldLater;
+ }
+ Debug::log("futex_wake yielding? {}", shouldYield);
+ }
// If this futex wake is dropping a priority boost, reset the boost.
if (shouldResetPrioirity)
@@ -551,12 +591,18 @@
priority_boost_for_thread(currentThread->id_get()));
// If we have dropped priority below that of another runnable thread, we
// should yield now.
- shouldYield |= !currentThread->is_highest_priority();
}
- if (shouldYield)
+ switch (shouldYield)
{
- yield();
+ case YieldLater:
+ Timer::ensure_tick();
+ break;
+ case YieldNow:
+ yield();
+ break;
+ case NoYield:
+ break;
}
return woke;
diff --git a/sdk/core/scheduler/thread.h b/sdk/core/scheduler/thread.h
index f937c46..e050e08 100644
--- a/sdk/core/scheduler/thread.h
+++ b/sdk/core/scheduler/thread.h
@@ -5,8 +5,10 @@
#include "common.h"
#include <cdefs.h>
+#include <platform-timer.hh>
#include <priv/riscv.h>
#include <strings.h>
+#include <tick_macros.h>
#include <utils.hh>
namespace
@@ -111,6 +113,15 @@
current->priority,
current->OriginalPriority);
}
+ if (current != th)
+ {
+ current->expiryTime = TimerCore::time();
+ }
+#ifdef CLANG_TIDY
+ // The static analyser thinks that `debug_log_message_write` may
+ // assign `nullptr` to `current`.
+ __builtin_assume(current != nullptr);
+#endif
return current->tStackPtr;
}
return schedTStack;
@@ -203,10 +214,9 @@
* the resource disappearing, do some clean-ups.
* @param the reason why this thread is now able to be scheduled
*/
- bool ready(WakeReason reason)
+ void ready(WakeReason reason)
{
int64_t ticksLeft;
- bool schedule = false;
// We must be suspended.
Debug::Assert(state == ThreadState::Suspended,
@@ -229,23 +239,14 @@
if (priority > highestPriority)
{
highestPriority = priority;
- schedule = true;
}
}
- // If this is the same priority as the current thread, we may need
- // to update the timer.
- if (priority >= highestPriority)
- {
- schedule = true;
- }
if (reason == WakeReason::Timer || reason == WakeReason::Delete)
{
multiWaiter = nullptr;
}
list_insert(&priorityList[priority]);
isYielding = false;
-
- return schedule;
}
/**
@@ -543,6 +544,17 @@
return next != this;
}
+ /**
+ * Returns true if the thread has run for a complete tick. This must
+ * be called only on the currently running thread.
+ */
+ bool has_run_for_full_tick()
+ {
+ Debug::Assert(this == current,
+ "Only the current thread is running");
+ return TimerCore::time() >= expiryTime + TIMERCYCLES_PER_TICK;
+ }
+
~ThreadImpl()
{
// We have static definition of threads. We only create threads in
@@ -564,8 +576,12 @@
///@}
/// Pointer to the list of the resource this thread is blocked on.
ThreadImpl **sleepQueue;
- /// If suspended, when will this thread expire. The maximum value is
- /// special-cased to mean blocked indefinitely.
+ /**
+ * If suspended, when will this thread expire. The maximum value is
+ * special-cased to mean blocked indefinitely.
+ *
+ * When a thread is running, this the time at which it was scheduled.
+ */
uint64_t expiryTime{static_cast<uint64_t>(-1)};
/// The number of cycles that this thread has been scheduled for.
diff --git a/sdk/core/scheduler/timer.h b/sdk/core/scheduler/timer.h
index c74a9d5..5a48924 100644
--- a/sdk/core/scheduler/timer.h
+++ b/sdk/core/scheduler/timer.h
@@ -3,6 +3,7 @@
#pragma once
+#include "common.h"
#include "plic.h"
#include "thread.h"
#include <platform-timer.hh>
@@ -23,6 +24,9 @@
{
T::setnext(cycles)
};
+ {
+ T::next()
+ } -> std::same_as<uint64_t>;
};
static_assert(
@@ -106,6 +110,21 @@
}
/**
+ * Ensure that a timer tick is scheduled for the current thread.
+ */
+ static void ensure_tick()
+ {
+ auto *thread = Thread::current_get();
+ Debug::Assert(thread != nullptr,
+ "Ensure tick called with no running thread");
+ auto tickTime = thread->expiryTime + TIMERCYCLES_PER_TICK;
+ if (tickTime < TimerCore::next())
+ {
+ setnext(tickTime);
+ }
+ }
+
+ /**
* Wake any threads that were sleeping until a timeout before the
* current time. This also wakes yielded threads if there are no
* runnable threads.
diff --git a/sdk/include/platform/generic-riscv/platform-timer.hh b/sdk/include/platform/generic-riscv/platform-timer.hh
index 5f3e226..6fce600 100644
--- a/sdk/include/platform/generic-riscv/platform-timer.hh
+++ b/sdk/include/platform/generic-riscv/platform-timer.hh
@@ -80,6 +80,18 @@
*pmtimercmphigh = nextTime >> 32;
}
+ /**
+ * Returns the time at which the next timer is scheduled.
+ */
+ static uint64_t next()
+ {
+ volatile uint32_t *pmtimercmphigh = pmtimercmp + 1;
+ uint64_t nextTimer = *pmtimercmphigh;
+ nextTimer <<= 32;
+ nextTimer |= *pmtimercmp;
+ return nextTimer;
+ }
+
static void clear()
{
volatile uint32_t *pmtimercmphigh = pmtimercmp + 1;