Performance Improvements Suggestion: Avoid `locks` for Singletons and for RootScope
Currently, Jab uses locks to guarantee Singleton is only created one time.
But alternative Lazy<T> is a faster alternative to lock.
Instead of
private Jab.Performance.Basic.Singleton.Singleton1? _ISingleton1;
Jab.Performance.Basic.Singleton.ISingleton1 IServiceProvider<Jab.Performance.Basic.Singleton.ISingleton1>.GetService()
{
if (_ISingleton1 == null)
lock (this)
if (_ISingleton1 == null){
_ISingleton1 = new Jab.Performance.Basic.Singleton.Singleton1();
}
return _ISingleton1;
}
The JAB uses this code
public ImprovedContainerSingleton()
{
_ISingleton1 = new(() => new Jab.Performance.Basic.Singleton.Singleton1());
}
private Lazy<Jab.Performance.Basic.Singleton.Singleton1> _ISingleton1;
Jab.Performance.Basic.Singleton.ISingleton1 IServiceProvider<Jab.Performance.Basic.Singleton.ISingleton1>.GetService() => _ISingleton1.Value;
We can see up to 35% of performance improvements.
| Method | NumbersOfCalls | NumbersOfClasses | Mean | Error | StdDev | Ratio | RatioSD |
|---|---|---|---|---|---|---|---|
| Jab | 1 | 1 | 3.190 ns | 0.3329 ns | 0.0182 ns | 1.00 | 0.00 |
| Improved_Jab | 1 | 1 | 1.968 ns | 0.1505 ns | 0.0082 ns | 0.62 | 0.00 |
| MEDI | 1 | 1 | 9.357 ns | 0.2965 ns | 0.0163 ns | 2.93 | 0.02 |
| Jab | 100 | 3 | 693.577 ns | 30.0415 ns | 1.6467 ns | 1.00 | 0.00 |
| Improved_Jab | 100 | 3 | 548.498 ns | 7.3230 ns | 0.4014 ns | 0.79 | 0.00 |
| MEDI | 100 | 3 | 2,909.288 ns | 33.8740 ns | 1.8567 ns | 4.19 | 0.01 |
See Details: Jab.Performance.Basic.Singleton.BasicSingletonBenchmark-report-github.md
The problem with Lazy is that all instances need to be eagerly initialized on container creation, leading to a very high startup cost especially for large container.
Maybe LazyInitializer.EnsureInitialized is the way to go here.
This is the Startup benchmark for the code above. (From PR #168)
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|
| Jab_Singleton | 9.048 ns | 4.151 ns | 0.2275 ns | 0.0067 | - | 56 B | 1.00 |
| Improved_Jab_Singleton | 47.049 ns | 6.874 ns | 0.3768 ns | 0.0488 | 0.0001 | 408 B | ? |
| MEDI_Singleton | 1,927.726 ns | 2,530.384 ns | 138.6989 ns | 0.8640 | 0.2155 | 7232 B | ? |
You are right, this change will cause a bigger startup timer and more allocation.
Benchmark when using LazyInitializer.EnsureInitialized
Singleton benchmark
| Method | NumbersOfCalls | NumbersOfClasses | Mean | Error | StdDev | Ratio | RatioSD |
|---|---|---|---|---|---|---|---|
| Jab | 1 | 1 | 3.437 ns | 3.4240 ns | 0.1877 ns | 1.00 | 0.00 |
| Improved_Jab | 1 | 1 | 1.833 ns | 0.4405 ns | 0.0241 ns | 0.53 | 0.04 |
| Improved2_Jab | 1 | 1 | 2.131 ns | 3.3616 ns | 0.1843 ns | 0.62 | 0.09 |
| MEDI | 1 | 1 | 9.669 ns | 6.6185 ns | 0.3628 ns | 2.82 | 0.18 |
| Jab | 100 | 3 | 689.601 ns | 2.0599 ns | 0.1129 ns | 1.00 | 0.00 |
| Improved_Jab | 100 | 3 | 628.522 ns | 1,178.7369 ns | 64.6105 ns | 0.91 | 0.09 |
| Improved2_Jab | 100 | 3 | 590.958 ns | 1,019.3358 ns | 55.8732 ns | 0.86 | 0.08 |
| MEDI | 100 | 3 | 2,935.810 ns | 1,653.3193 ns | 90.6240 ns | 4.26 | 0.13 |
Startup benchmark
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|
| Jab_Singleton | 9.702 ns | 7.7862 ns | 0.4268 ns | 0.0067 | - | 56 B | 1.00 |
| Improved_Jab_Singleton | 55.588 ns | 76.3415 ns | 4.1845 ns | 0.0488 | 0.0001 | 408 B | ? |
| Improved_2_Jab_Singleton | 8.722 ns | 0.1648 ns | 0.0090 ns | 0.0067 | - | 56 B | ? |
| MEDI_Singleton | 2,197.545 ns | 1,339.7199 ns | 73.4346 ns | 0.8640 | 0.2155 | 7232 B | ? |
Code
private Jab.Performance.Basic.Singleton.Singleton1? _ISingleton1;
Jab.Performance.Basic.Singleton.ISingleton1 IServiceProvider<Jab.Performance.Basic.Singleton.ISingleton1>.GetService() => LazyInitializer.EnsureInitialized<Jab.Performance.Basic.Singleton.Singleton1>(ref this._ISingleton1);
See: https://github.com/pakrym/jab/pull/168#discussion_r1465940230