From dae61776db71f04cd24b9e12ee6c8e8deebcd703 Mon Sep 17 00:00:00 2001 From: Eideren Date: Wed, 17 Nov 2021 17:07:35 +0100 Subject: [PATCH 1/2] [Threading] Threadpool, lifo semaphore --- .../Threading/ThreadPool.SemaphoreW.cs | 9 ++- .../core/Stride.Core/Threading/ThreadPool.cs | 74 ++++++++++++++++--- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/sources/core/Stride.Core/Threading/ThreadPool.SemaphoreW.cs b/sources/core/Stride.Core/Threading/ThreadPool.SemaphoreW.cs index 2ee9908462..5162175c77 100644 --- a/sources/core/Stride.Core/Threading/ThreadPool.SemaphoreW.cs +++ b/sources/core/Stride.Core/Threading/ThreadPool.SemaphoreW.cs @@ -15,7 +15,7 @@ public sealed partial class ThreadPool /// /// Mostly lifted from dotnet's LowLevelLifoSemaphore /// - private class SemaphoreW + private class SemaphoreW : ISemaphore { private const int SpinSleep0Threshold = 10; @@ -35,6 +35,8 @@ private class SemaphoreW static SemaphoreW() { // Workaround as Thread.OptimalMaxSpinWaitsPerSpinIteration is internal and only implemented in core + // Note that as of .Net6 (https://github.com/dotnet/runtime/issues/53509 & https://github.com/dotnet/runtime/pull/55295) + // the underlying value is periodically updated in case the timing changes, this property shouldn't be used under those runtimes BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static; var f = typeof(Thread).GetProperty("OptimalMaxSpinWaitsPerSpinIteration", flags); int opti = 7; @@ -57,6 +59,11 @@ public SemaphoreW(int spinCountParam) lifoSemaphore = new Semaphore(0, int.MaxValue); } + + public void Dispose() + { + lifoSemaphore?.Dispose(); + } public void Wait(int timeout = -1) => internals.Wait(spinCount, lifoSemaphore, timeout); diff --git a/sources/core/Stride.Core/Threading/ThreadPool.cs b/sources/core/Stride.Core/Threading/ThreadPool.cs index bcc0aea638..5fb0c2355a 100644 --- a/sources/core/Stride.Core/Threading/ThreadPool.cs +++ b/sources/core/Stride.Core/Threading/ThreadPool.cs @@ -4,6 +4,8 @@ using Stride.Core.Annotations; using System; using System.Collections.Concurrent; +using System.Reflection; +using System.Runtime.InteropServices; using System.Threading; namespace Stride.Core.Threading @@ -19,14 +21,15 @@ public sealed partial class ThreadPool : IDisposable /// public static ThreadPool Instance = new ThreadPool(); + /// Is the thread reading this property a worker thread + public static bool IsWorkerThread => isWorkerThread; + private static readonly bool SingleCore; [ThreadStatic] - private static bool isWorkedThread; - /// Is the thread reading this property a worker thread - public static bool IsWorkedThread => isWorkedThread; + private static bool isWorkerThread; private readonly ConcurrentQueue workItems = new ConcurrentQueue(); - private readonly SemaphoreW semaphore; + private readonly ISemaphore semaphore; private long completionCounter; private int workScheduled, threadsBusy; @@ -44,8 +47,29 @@ public sealed partial class ThreadPool : IDisposable public ThreadPool(int? threadCount = null) { - semaphore = new SemaphoreW(spinCountParam:70); - + int spinCount = 70; + if(RuntimeInformation.ProcessArchitecture is Architecture.Arm or Architecture.Arm64) + { + // Dotnet: + // On systems with ARM processors, more spin-waiting seems to be necessary to avoid perf regressions from incurring + // the full wait when work becomes available soon enough. This is more noticeable after reducing the number of + // thread requests made to the thread pool because otherwise the extra thread requests cause threads to do more + // busy-waiting instead and adding to contention in trying to look for work items, which is less preferable. + spinCount *= 4; + } + try + { + semaphore = new DotnetLifoSemaphore(spinCount); + } + catch + { + // For net6+ this should not happen, logging instead of throwing as this is just a performance regression + if(Environment.Version.Major >= 6) + Console.Out?.WriteLine($"{typeof(ThreadPool).FullName}: Falling back to suboptimal semaphore"); + + semaphore = new SemaphoreW(spinCountParam:70); + } + WorkerThreadsCount = threadCount ?? (Environment.ProcessorCount == 1 ? 1 : Environment.ProcessorCount - 1); leftToDispose = WorkerThreadsCount; for (int i = 0; i < WorkerThreadsCount; i++) @@ -129,7 +153,7 @@ private void NewWorker() private void WorkerThreadScope() { - isWorkedThread = true; + isWorkerThread = true; try { do @@ -170,12 +194,9 @@ public void Dispose() } semaphore.Release(WorkerThreadsCount); + semaphore.Dispose(); while (Volatile.Read(ref leftToDispose) != 0) { - if (semaphore.SignalCount == 0) - { - semaphore.Release(1); - } Thread.Yield(); } @@ -185,5 +206,36 @@ public void Dispose() } } + + + + private interface ISemaphore : IDisposable + { + public void Release( int Count ); + public void Wait( int timeout = - 1 ); + } + + + + private class DotnetLifoSemaphore : ISemaphore + { + private readonly IDisposable semaphore; + private readonly Func wait; + private readonly Action release; + + public DotnetLifoSemaphore(int spinCount) + { + Type lifoType = Type.GetType("System.Threading.LowLevelLifoSemaphore"); + semaphore = Activator.CreateInstance(lifoType, new object[]{ 0, short.MaxValue, spinCount, new Action( () => {} ) }) as IDisposable; + wait = lifoType.GetMethod("Wait", BindingFlags.Instance | BindingFlags.Public).CreateDelegate>(semaphore); + release = lifoType.GetMethod("Release", BindingFlags.Instance | BindingFlags.Public).CreateDelegate>(semaphore); + } + + + + public void Dispose() => semaphore.Dispose(); + public void Release(int count) => release(count); + public void Wait(int timeout = -1) => wait(timeout, true); + } } } From b1d409c41c457d8c728c6f45ed0507b7ee2a89d3 Mon Sep 17 00:00:00 2001 From: Eideren Date: Wed, 17 Nov 2021 21:52:26 +0100 Subject: [PATCH 2/2] [Threading] Seal threadpool's internal classes --- sources/core/Stride.Core/Threading/ThreadPool.SemaphoreW.cs | 2 +- sources/core/Stride.Core/Threading/ThreadPool.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/core/Stride.Core/Threading/ThreadPool.SemaphoreW.cs b/sources/core/Stride.Core/Threading/ThreadPool.SemaphoreW.cs index 5162175c77..66c8a05440 100644 --- a/sources/core/Stride.Core/Threading/ThreadPool.SemaphoreW.cs +++ b/sources/core/Stride.Core/Threading/ThreadPool.SemaphoreW.cs @@ -15,7 +15,7 @@ public sealed partial class ThreadPool /// /// Mostly lifted from dotnet's LowLevelLifoSemaphore /// - private class SemaphoreW : ISemaphore + private sealed class SemaphoreW : ISemaphore { private const int SpinSleep0Threshold = 10; diff --git a/sources/core/Stride.Core/Threading/ThreadPool.cs b/sources/core/Stride.Core/Threading/ThreadPool.cs index 5fb0c2355a..8d28026c44 100644 --- a/sources/core/Stride.Core/Threading/ThreadPool.cs +++ b/sources/core/Stride.Core/Threading/ThreadPool.cs @@ -217,7 +217,7 @@ private interface ISemaphore : IDisposable - private class DotnetLifoSemaphore : ISemaphore + private sealed class DotnetLifoSemaphore : ISemaphore { private readonly IDisposable semaphore; private readonly Func wait;