jab icon indicating copy to clipboard operation
jab copied to clipboard

Performance Improvements Suggestion: Avoid `locks` for Singletons and for RootScope

Open rafaelsc opened this issue 2 years ago • 4 comments

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

rafaelsc avatar Jan 18 '24 06:01 rafaelsc

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.

pakrym avatar Jan 21 '24 18:01 pakrym

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.

rafaelsc avatar Jan 22 '24 06:01 rafaelsc

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);

rafaelsc avatar Jan 22 '24 07:01 rafaelsc

See: https://github.com/pakrym/jab/pull/168#discussion_r1465940230

rafaelsc avatar Jan 28 '24 19:01 rafaelsc