Kernel: Wake cores from idle directly rather than through a host thread

Right now when a core enters an idle state, leaving that idle state requires us to first signal the core's idle thread, which then signals the correct thread that we want to run on the core. This means that in a lot of cases, we're paying double for a thread to be woken from an idle state.

This PR moves this process to happen on the thread that is waking others out of idle, instead of an idle thread that needs to be woken first.

For compatibility the process has been kept as similar as possible - the process for IdleThreadLoop has been migrated to TryLeaveIdle, and is gated by a condition variable that lets it run only once at a time for each core. A core is only considered for wake from idle if idle is both active and has been signalled - the signal is consumed and the active state is cleared when the core leaves idle.

Dummy threads (just the idle thread at the moment) have been changed to have no host thread, as the work is now done by threads entering idle and signalling out of it.

This could put a bit of extra work on threads that would have triggered `_idleInterruptEvent` before, but I'd expect less work than signalling all those reset events and the OS overhead that follows. Worst case is that other threads performing these signals at the same time will have to wait for each other, but it's still going to be a very short amount of time.

Improvements are best seen in games with heavy (or very misguided) multithreading, such as Pokemon: Legends Arceus. Improvements are expected in Scarlet/Violet and TOTK, but are harder to measure.

Testing on Linux/MacOS still to be done, definitely need to test more games as this affects all of them (obviously) and any issues might be rare to encounter.
This commit is contained in:
riperiperi 2024-05-19 21:32:47 +01:00
parent eb1ce41b00
commit c0c81d4c9d
3 changed files with 100 additions and 45 deletions

View File

@ -1,6 +1,7 @@
using Ryujinx.Common;
using Ryujinx.HLE.HOS.Kernel.Process;
using System;
using System.Diagnostics;
using System.Numerics;
using System.Threading;
@ -28,13 +29,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
private SchedulingState _state;
private AutoResetEvent _idleInterruptEvent;
private readonly object _idleInterruptEventLock;
private KThread _previousThread;
private KThread _currentThread;
private readonly KThread _idleThread;
private int _coreIdleLock;
private bool _idleSignalled = true;
private bool _idleActive = true;
public KThread PreviousThread => _previousThread;
public KThread CurrentThread => _currentThread;
public long LastContextSwitchTime { get; private set; }
@ -45,23 +47,17 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
_context = context;
_coreId = coreId;
_idleInterruptEvent = new AutoResetEvent(false);
_idleInterruptEventLock = new object();
KThread idleThread = CreateIdleThread(context, coreId);
_currentThread = idleThread;
_idleThread = idleThread;
idleThread.StartHostThread();
idleThread.SchedulerWaitEvent.Set();
}
private KThread CreateIdleThread(KernelContext context, int cpuCore)
{
KThread idleThread = new(context);
idleThread.Initialize(0UL, 0UL, 0UL, PrioritiesCount, cpuCore, null, ThreadType.Dummy, IdleThreadLoop);
idleThread.Initialize(0UL, 0UL, 0UL, PrioritiesCount, cpuCore, null, ThreadType.Dummy, null);
return idleThread;
}
@ -243,33 +239,57 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
}
// If the core is idle, ensure that the idle thread is awaken.
context.Schedulers[coreToSignal]._idleInterruptEvent.Set();
context.Schedulers[coreToSignal].NotifyIdleThread();
scheduledCoresMask &= ~(1UL << coreToSignal);
}
}
private void IdleThreadLoop()
private void ActivateIdleThread()
{
while (_context.Running)
while (Interlocked.CompareExchange(ref _coreIdleLock, 1, 0) != 0)
{ }
Thread.MemoryBarrier();
// Signals that idle thread is now active on this core.
_idleActive = true;
TryLeaveIdle();
Interlocked.Exchange(ref _coreIdleLock, 0);
}
private void NotifyIdleThread()
{
while (Interlocked.CompareExchange(ref _coreIdleLock, 1, 0) != 0)
{ }
Thread.MemoryBarrier();
// Signals that the idle core may be able to exit idle.
_idleSignalled = true;
TryLeaveIdle();
Interlocked.Exchange(ref _coreIdleLock, 0);
}
public void TryLeaveIdle()
{
if (_idleSignalled && _idleActive)
{
_state.NeedsScheduling = false;
Thread.MemoryBarrier();
KThread nextThread = PickNextThread(_state.SelectedThread);
KThread nextThread = PickNextThread(_idleThread, _state.SelectedThread);
if (_idleThread != nextThread)
{
_idleThread.SchedulerWaitEvent.Reset();
WaitHandle.SignalAndWait(nextThread.SchedulerWaitEvent, _idleThread.SchedulerWaitEvent);
_idleActive = false;
nextThread.SchedulerWaitEvent.Set();
}
_idleInterruptEvent.WaitOne();
}
lock (_idleInterruptEventLock)
{
_idleInterruptEvent.Dispose();
_idleInterruptEvent = null;
_idleSignalled = false;
}
}
@ -286,26 +306,45 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
return;
}
Debug.Assert(currentThread != _idleThread);
currentThread.SchedulerWaitEvent.Reset();
currentThread.ThreadContext.Unlock();
// Wake all the threads that might be waiting until this thread context is unlocked.
for (int core = 0; core < CpuCoresCount; core++)
{
_context.Schedulers[core]._idleInterruptEvent.Set();
_context.Schedulers[core].NotifyIdleThread();
}
KThread nextThread = PickNextThread(selectedThread);
KThread nextThread = PickNextThread(KernelStatic.GetCurrentThread(), selectedThread);
if (currentThread.Context.Running)
{
// Wait until this thread is scheduled again, and allow the next thread to run.
WaitHandle.SignalAndWait(nextThread.SchedulerWaitEvent, currentThread.SchedulerWaitEvent);
if (nextThread == _idleThread)
{
ActivateIdleThread();
currentThread.SchedulerWaitEvent.WaitOne();
}
else
{
WaitHandle.SignalAndWait(nextThread.SchedulerWaitEvent, currentThread.SchedulerWaitEvent);
}
}
else
{
// Allow the next thread to run.
nextThread.SchedulerWaitEvent.Set();
if (nextThread == _idleThread)
{
ActivateIdleThread();
}
else
{
nextThread.SchedulerWaitEvent.Set();
}
// We don't need to wait since the thread is exiting, however we need to
// make sure this thread will never call the scheduler again, since it is
@ -319,7 +358,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
}
}
private KThread PickNextThread(KThread selectedThread)
private KThread PickNextThread(KThread currentThread, KThread selectedThread)
{
while (true)
{
@ -335,7 +374,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
// on the core, as the scheduled thread will handle the next switch.
if (selectedThread.ThreadContext.Lock())
{
SwitchTo(selectedThread);
SwitchTo(currentThread, selectedThread);
if (!_state.NeedsScheduling)
{
@ -353,7 +392,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
{
// The core is idle now, make sure that the idle thread can run
// and switch the core when a thread is available.
SwitchTo(null);
SwitchTo(currentThread, null);
return _idleThread;
}
@ -363,10 +402,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
}
}
private void SwitchTo(KThread nextThread)
private void SwitchTo(KThread currentThread, KThread nextThread)
{
KProcess currentProcess = KernelStatic.GetCurrentProcess();
KThread currentThread = KernelStatic.GetCurrentThread();
KProcess currentProcess = currentThread.Owner;
nextThread ??= _idleThread;
@ -645,11 +683,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
public void Dispose()
{
// Ensure that the idle thread is not blocked and can exit.
lock (_idleInterruptEventLock)
{
_idleInterruptEvent?.Set();
}
// No resources to dispose for now.
}
}
}

View File

@ -6,6 +6,7 @@ using Ryujinx.HLE.HOS.Kernel.SupervisorCall;
using Ryujinx.Horizon.Common;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Threading;
@ -44,8 +45,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
private ulong _entrypoint;
private ThreadStart _customThreadStart;
private bool _forcedUnschedulable;
private bool _isDummy;
public bool IsSchedulable => _customThreadStart == null && !_forcedUnschedulable;
public bool IsSchedulable => _customThreadStart == null && !_isDummy && !_forcedUnschedulable;
public ulong MutexAddress { get; set; }
public int KernelWaitersCount { get; private set; }
@ -143,7 +145,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
PreferredCore = cpuCore;
AffinityMask |= 1UL << cpuCore;
SchedFlags = type == ThreadType.Dummy
_isDummy = type == ThreadType.Dummy;
SchedFlags = _isDummy
? ThreadSchedState.Running
: ThreadSchedState.None;
@ -183,7 +187,10 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
is64Bits = true;
}
HostThread = new Thread(ThreadStart);
if (!_isDummy)
{
HostThread = new Thread(ThreadStart);
}
Context = owner?.CreateExecutionContext() ?? new ProcessExecutionContext();
@ -207,7 +214,10 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
ThreadUid = KernelContext.NewThreadUid();
HostThread.Name = customThreadStart != null ? $"HLE.OsThread.{ThreadUid}" : $"HLE.GuestThread.{ThreadUid}";
if (!_isDummy)
{
HostThread.Name = customThreadStart != null ? $"HLE.OsThread.{ThreadUid}" : $"HLE.GuestThread.{ThreadUid}";
}
_hasBeenInitialized = true;
@ -1055,6 +1065,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
// If the thread is not schedulable, we want to just run or pause
// it directly as we don't care about priority or the core it is
// running on in this case.
Debug.Assert(!_isDummy);
if (SchedFlags == ThreadSchedState.Running)
{
_schedulerWaitEvent.Set();
@ -1231,6 +1244,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
public void StartHostThread()
{
if (_isDummy)
{
throw new InvalidOperationException("Dummy threads can't start a host thread.");
}
if (_schedulerWaitEvent == null)
{
var schedulerWaitEvent = new ManualResetEvent(false);

View File

@ -515,11 +515,14 @@ namespace Ryujinx.HLE.HOS.Services
{
if (disposing && _selfThread != null)
{
if (_selfThread.HostThread.ManagedThreadId != Environment.CurrentManagedThreadId && _selfThread.HostThread.Join(_threadJoinTimeout) == false)
if (_selfThread.HostThread != null)
{
Logger.Warning?.Print(LogClass.Service, $"The ServerBase thread didn't terminate within {_threadJoinTimeout:g}, waiting longer.");
if (_selfThread.HostThread.ManagedThreadId != Environment.CurrentManagedThreadId && _selfThread.HostThread.Join(_threadJoinTimeout) == false)
{
Logger.Warning?.Print(LogClass.Service, $"The ServerBase thread didn't terminate within {_threadJoinTimeout:g}, waiting longer.");
_selfThread.HostThread.Join(Timeout.Infinite);
_selfThread.HostThread.Join(Timeout.Infinite);
}
}
if (Interlocked.Exchange(ref _isDisposed, 1) == 0)