From 8cb5625689f0f021939b47124932c362bdc44c49 Mon Sep 17 00:00:00 2001 From: Sergey Teplyakov Date: Wed, 7 Jan 2026 13:47:58 -0500 Subject: [PATCH 1/3] Add AggressiveInlining to EnsureInitializedCore method to allow delegates stack alloc Currently stack allocation (and overall de-abstraction) works only when JIT can has a full understanding of the code. And due to lack of cross-prodcedural analysis it means that for the optimization to work the operations should be inlined. The same is true for `LazyInitializer.EnsureInitialized` that takes a delegate. Currently, because `EnsureInitializedCore` quite big and can't be inlined by default, JIT can't stack allocate the delegate passed to the method. The following benchmark shows the impact of this change: ```csharp [MemoryDiagnoser] [ShortRunJob(RuntimeMoniker.Net90)] [ShortRunJob(RuntimeMoniker.Net10_0)] [CategoriesColumn] [HideColumns(Column.Job, Column.Error, Column.Median, Column.RatioSD, Column.Ratio, Column.AllocRatio, Column.StdDev, Column.Gen0)] public class BenchmarkingDelegates { [Benchmark] public string EnsureInitialized_Old() { string defaultValue = "defaultValue"; return LazyInitializer.EnsureInitialized(ref _cachedValue, () => defaultValue); } [Benchmark] public string EnsureInitialized_New() { string defaultValue = "defaultValue"; return MyLazyInitializer.EnsureInitialized(ref _cachedValue, () => defaultValue); } ``` The output: ```csharp | Method | Runtime | Mean | Allocated | |---------------------- |---------- |---------:|----------:| | EnsureInitialized_Old | .NET 10.0 | 5.374 ns | 88 B | | EnsureInitialized_New | .NET 10.0 | 1.951 ns | 24 B | | EnsureInitialized_Old | .NET 9.0 | 6.194 ns | 88 B | | EnsureInitialized_New | .NET 9.0 | 6.041 ns | 88 B | ``` The `MyLazyInitializer` is the copy of the existing one with `AggressiveInlining` flag set. --- .../src/System/Threading/LazyInitializer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/LazyInitializer.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/LazyInitializer.cs index 7b570f05a9557d..e143b7312d4765 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/LazyInitializer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/LazyInitializer.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace System.Threading { @@ -109,6 +110,7 @@ public static T EnsureInitialized([NotNull] ref T? target, Func valueFacto /// The variable that need to be initialized /// The delegate that will be executed to initialize the target /// The initialized variable + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static T EnsureInitializedCore([NotNull] ref T? target, Func valueFactory) where T : class { T value = valueFactory() ?? throw new InvalidOperationException(SR.Lazy_StaticInit_InvalidOperation); From 5d8df04bdbd8be2d70ed937f38969e0f09ab1299 Mon Sep 17 00:00:00 2001 From: Sergey Teplyakov Date: Thu, 8 Jan 2026 07:04:49 -0800 Subject: [PATCH 2/3] Refactor EnsureInitializedCore to use Throw method Checking if extracting `throw` helps JIT to inline the method without using `MethodImpl` attribute. --- .../src/System/Threading/LazyInitializer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/LazyInitializer.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/LazyInitializer.cs index e143b7312d4765..716c30bf2b960a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/LazyInitializer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/LazyInitializer.cs @@ -110,13 +110,14 @@ public static T EnsureInitialized([NotNull] ref T? target, Func valueFacto /// The variable that need to be initialized /// The delegate that will be executed to initialize the target /// The initialized variable - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static T EnsureInitializedCore([NotNull] ref T? target, Func valueFactory) where T : class { - T value = valueFactory() ?? throw new InvalidOperationException(SR.Lazy_StaticInit_InvalidOperation); + T value = valueFactory() ?? Throw(); Interlocked.CompareExchange(ref target, value, null!); Debug.Assert(target != null); return target; + + static T Throw() => throw new InvalidOperationException(SR.Lazy_StaticInit_InvalidOperation); } /// From 316d2531e2285bab08e242eae8f5d6bd8af80cf4 Mon Sep 17 00:00:00 2001 From: Sergey Teplyakov Date: Thu, 8 Jan 2026 10:41:40 -0500 Subject: [PATCH 3/3] Update src/libraries/System.Private.CoreLib/src/System/Threading/LazyInitializer.cs Co-authored-by: Stephen Toub --- .../src/System/Threading/LazyInitializer.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/LazyInitializer.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/LazyInitializer.cs index 716c30bf2b960a..b2fddf32f8cfba 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/LazyInitializer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/LazyInitializer.cs @@ -112,12 +112,18 @@ public static T EnsureInitialized([NotNull] ref T? target, Func valueFacto /// The initialized variable private static T EnsureInitializedCore([NotNull] ref T? target, Func valueFactory) where T : class { - T value = valueFactory() ?? Throw(); + T? value = valueFactory(); + if (value is null) + { + Throw(); + } + Interlocked.CompareExchange(ref target, value, null!); Debug.Assert(target != null); return target; - static T Throw() => throw new InvalidOperationException(SR.Lazy_StaticInit_InvalidOperation); + [DoesNotReturn] + static void Throw() => throw new InvalidOperationException(SR.Lazy_StaticInit_InvalidOperation); } ///