I migrated a project from .NET 6 to .NET 8 and encountered an issue with CPU performance when using ReaderWriterLockSlim
. With .NET 8, CPU consumption is way higher (4-5x higher) than .NET 6. I have used below code sample to test the CPU performance:
The code sample used for testing was generated using ChatGPT:
public class ReaderWriterSlimTests
{
private static readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private static int _sharedResource = 0;
private static readonly int ReaderCount = Environment.ProcessorCount * 2;
private static readonly int WriterCount = Environment.ProcessorCount / 2;
private static readonly int Iterations = 1_000_000;
private static volatile bool _running = true;
public void RunTests()
{
Console.WriteLine($"Running test on .NET {Environment.Version}");
Console.WriteLine($"Readers: {ReaderCount}, Writers: {WriterCount}, Iterations: {Iterations}");
Stopwatch stopwatch = Stopwatch.StartNew();
Process process = Process.GetCurrentProcess();
TimeSpan startCpuTime = process.TotalProcessorTime;
Thread[] readers = new Thread[ReaderCount];
Thread[] writers = new Thread[WriterCount];
// Start readers
for (int i = 0; i < ReaderCount; i++)
{
readers[i] = new Thread(ReaderTask);
readers[i].Start();
}
// Start writers
for (int i = 0; i < WriterCount; i++)
{
writers[i] = new Thread(WriterTask);
writers[i].Start();
}
// Run for a few seconds
Thread.Sleep(30000);
_running = false;
// Wait for all threads to finish
foreach (var reader in readers) reader.Join();
foreach (var writer in writers) writer.Join();
stopwatch.Stop();
TimeSpan endCpuTime = process.TotalProcessorTime;
double cpuUsage = (endCpuTime - startCpuTime).TotalMilliseconds / stopwatch.ElapsedMilliseconds * 100;
Console.WriteLine($"Elapsed Time: {stopwatch.ElapsedMilliseconds} ms");
Console.WriteLine($"CPU Usage Approximation: {cpuUsage:F2}%");
}
private void ReaderTask()
{
while (_running)
{
_lock.EnterReadLock();
var value = _sharedResource; // Simulate read
_lock.ExitReadLock();
}
}
private void WriterTask()
{
Random rnd = new Random();
while (_running)
{
_lock.EnterWriteLock();
_sharedResource = rnd.Next(); // Simulate write
_lock.ExitWriteLock();
}
}
}
If I run this code in .NET 6, CPU usage is around 300%. Same code in .NET 8 is around 1800%.
Any idea what has caused this performance degradation in .NET 8? Also, going forward, how to fix this issue in .NET 8?
Thanks Vik
There was no performance degradation, quite the opposite - performance improved in .NET 7 by fixing a bug that caused long latencies in .NET 6. The ChatGPT code doesn't measure the actual execution time, so the average CPU usage isn't useful.
ReaderWriterLockSlim
is meant to peg a core at 100%, because it uses spinwaiting instead of putting a thread to sleep and thus causing it to get evicted from a core. Evicting a thread and rescheduling is an expensive operation in any OS and there's no way to know when the OS will reschedule a thread for execution. The scheduler on Windows runs roughly every 15ms, so the blocked thread may have to wait that much or more to get rescheduled.
All Slim
versions of any concurrency objects use spinwaiting to avoid getting the thread evicted. All of them are meant for scenarios where the delay is expected to be small, definitely smaller than the 15ms of a scheduling quant.
There was a bug in .NET 6, which caused ReaderWriterLockSlim
to not spin enough, leading to long delays - a Thread.Sleep(0)/Thread.Sleep(1)
caused the thread to get evicted : There may be unexpectedly high delays in ReaderWriterLockSlim operations. The bug's reproduction code shows a huge delay in tight loops - 15-45ms delays which suggest that 1-2 rescheduling chances were missed.
That was fixed in .NET 7 by replacing Thread.Sleep
with adaptive code that keeps using Thread.Sleep()
in single-core machines, or a spinwait with exponential backoff in multi-core machines.
In short, there's no bug to fix and no plans to reintroduce the .NET 6 bug.