Tri*_*nko 1 c# performance locking
对于一千万次迭代,我正在比较访问单例的不同方式。
直接读取静态属性是最快的。
Lazy<T>,其Value属性在内部引入了空检查、装箱值的强制转换以及装箱值的访问需要更长的时间(2x - 3x)。
我发现令人吃惊的是,一个常见的双重检查锁定模式(对于大多数访问实际上只是空检查)花费的时间要长得多(在某些情况下几乎是 5 倍)——远比预期的要长。我还震惊地发现,简单地注释掉 'lock' 语句,即使它从未被击中,也可以减少接近静态属性的基本直接访问的时间,并为空检查增加了一点额外的开销。
仅仅在方法中存在这个“lock”语句,即使 lock 语句从未执行过,也会导致它花费更多时间,这有什么意义?我在此测试代码中预先分配了单例,因此永远不会命中锁定语句;然而,简单地取消对“锁定”行的注释会增加运行时间。我没有解释。
这是一个简短的示例,您可以将其粘贴到控制台应用程序中并运行以查看几个不同访问的时间。
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
namespace SingletonInitializationMethodPerformanceTest
{
public class Program
{
private const int TestRuns = 5;
private const int AccessesPerTestRun = 10000000;
private static int _totalAccesses;
private static readonly Stopwatch Timer = new Stopwatch();
private static readonly Func<Singleton>[] Tests = { Singleton.LazyInstance, Singleton.InstanceWithLockPresent, Singleton.InstanceWithLockCommentedOut, Singleton.InstanceWithLockInSeparateMethod, Singleton.DirectlyReturnedInstance };
public static void Main(string[] args)
{
var results = Tests.Select(x => new int[TestRuns]).ToList();
Thread.CurrentThread.Priority = ThreadPriority.Highest; Thread.Sleep(3000);
for (var iRun = 0; iRun < TestRuns; iRun++)
for (var iTest = 0; iTest < Tests.Length; iTest++)
results[iTest][iRun] = RunTest(Tests[iTest]);
Console.Write($"Total accesses: {_totalAccesses}\n\n");
for (var iTest = 0; iTest < Tests.Length; iTest++)
Console.Write($"{Tests[iTest].Method.Name.PadRight(35, ' ')} [{string.Join(",", results[iTest].Select(x => x.ToString().PadLeft(5, ' ')))}]\n");
Console.ReadLine();
}
private static int RunTest(Func<Singleton> getSingleton)
{
Timer.Restart();
for (var i = 0; i < AccessesPerTestRun; i++)
_totalAccesses += getSingleton().Value;
Timer.Stop();
return (int)Timer.Elapsed.TotalMilliseconds;
}
}
public class Singleton
{
private static readonly object _initializationLock = new object();
private static Singleton _instance = new Singleton(); //Instance is pre-assigned for testing purposes, ensuring 'lock' statement is never hit; demonstrating the statement's presence alone increases runtime.
private static readonly Lazy<Singleton> _lazyInstance = new Lazy<Singleton>(() => new Singleton(), LazyThreadSafetyMode.ExecutionAndPublication);
public static Singleton LazyInstance() => _lazyInstance.Value;
public static Singleton InstanceWithLockPresent()
{
if (_instance == null)
{
lock (_initializationLock) //intentionally unreachable code; represents code that would be here if we were not pre-assigning the singleton for testing
{
if (_instance == null)
{
_instance = new Singleton();
}
}
}
return _instance;
}
public static Singleton InstanceWithLockCommentedOut()
{
if (_instance == null)
{
//lock (_initializationLock) //intentionally unreachable code; represents code that would be here if we were not pre-assigning the singleton for testing
{
if (_instance == null)
{
_instance = new Singleton();
}
}
}
return _instance;
}
public static Singleton InstanceWithLockInSeparateMethod() => _instance ?? DoLock();
private static Singleton DoLock()
{
lock (_initializationLock)
{
if (_instance == null)
{
_instance = new Singleton();
}
}
return _instance;
}
public static Singleton DirectlyReturnedInstance() => _instance;
public int Value => 1;
private Singleton()
{
}
}
}
Run Code Online (Sandbox Code Playgroud)
下面是一些示例运行时(每 1000 万次迭代的总毫秒数,对于访问单例的每种方法重复五次)。
很可能是因为锁引入了 try/finally、本地和相当多的代码 - 加上内存屏障语义,使 JIT 不想再内联您的方法?
这里的一个常见技巧是在 main 方法中有成功路径,在不同的方法中有“双重检查”失败路径,所以所有这些开销只会在 null 情况下执行。例如:
return _doubleCheckInstance ?? SlowPath();
Run Code Online (Sandbox Code Playgroud)
使用单独的SlowPath方法完成所有繁重的工作,包括锁定和新的