-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Fix #3862 - Double-Check locked patterns and consider static or Lazy<T> for Singletons #16216
Conversation
…T> for Singletons
|
||
private static readonly object s_singletonLock = new object(); | ||
private static volatile RuntimeBinder s_instance; | ||
private static Lazy<RuntimeBinder> s_lazyInstance = new Lazy<RuntimeBinder>(() => new RuntimeBinder()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: readonly
private static readonly Lazy<Hashtable> s_propertyCacheLazy = new Lazy<Hashtable>(() => new Hashtable()); | ||
private static readonly Lazy<Hashtable> s_eventCacheLazy = new Lazy<Hashtable>(() => new Hashtable()); | ||
private static readonly Lazy<Hashtable> s_attributeCacheLazy = new Lazy<Hashtable>(() => new Hashtable()); | ||
private static readonly Lazy<Hashtable> s_extendedPropertyCacheLazy = new Lazy<Hashtable>(() => new Hashtable()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think all of these would be better off using LazyInitializer.EnsureInitialized rather than Lazy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stephentoub Thanks. I will change this and use it in similar places...
Just to make sure I understand correctly, LazyInitializer.EnsureInitialized
could be preferred over Lazy<T>
in all the changes that I am / will be doing? (ref.: this link) Rules to use LazyInitializer.EnsureInitialized
could be,
- Places where the init code is not expected to throw exception that should be stored for later use, else use
Lazy<T>
. (Lazy<T>.Value
would make it available later on as well). - In case the init code is expensive (in terms of duplicate unused objects that would end up into GC, or changes to static state fields that need synchronization), it would be better to use the overload of
LazyInitializer.EnsureInitialized
that accepts a lock.
[A side thought: A Property get-accessor would be a typical way to wrap LazyInitializer.EnsureInitialized call supported by a backing field. Rosyln proposals 8364 or others might help reduce the verbosity in this case as well and avoid getting direct access to backing field bypassing the getter.
private static Hashtable s_propertyCache private static Hashtable PropertyCache => LazyInitializer.EnsureInitialized(ref s_propertyCache, () => new Hashtable());
could be reduced to following which would eliminate direct access
private static Hashtable PropertyCache => LazyInitializer.EnsureInitialized(ref field, () => new Hashtable()); where
field is the placeholder for auto-generated backing field
... just a thought]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stephentoub I edited my above comment. Wasn't sure if you would get another notification for the edits... hence this comment to bump the thread :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to make sure I understand correctly, LazyInitializer.EnsureInitialized could be preferred over Lazy in all the changes that I am / will be doing?
At least all of the ones where the object in question is a Hashtable. There's one that's involving a RuntimeBinder, for example, and comments in the ctor for RuntimeBinder suggests it's very expensive to create one, so that might be better as a Lazy<T>
. The issue is that constructing a Lazy<T>
itself has a bit of cost, e.g. it's at least an allocation, so we have to weigh that against no allocation but an optimistic concurrency approach where in the worst case you could have a bunch of threads all racing to initialize and each end up allocating the very expensive thing.
@@ -18,7 +18,13 @@ namespace System.Drawing | |||
public class ColorConverter : TypeConverter | |||
{ | |||
private static object s_valuesLock = new object(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is s_valuesLock used anywhere after your change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops :)
…T> for Singletons Changes to use LazyInitializer.EnsureInitialized instead of Lazy<T> for light weight objects.
@@ -352,7 +331,7 @@ private static Hashtable GetEditorTable(Type editorBaseType) | |||
// actually run. | |||
// | |||
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(editorBaseType.TypeHandle); | |||
table = s_editorTables[editorBaseType]; | |||
table = EditorTables[editorBaseType]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None of these (this one or the one below) need to change from s_editorTables to EditorTables: we know it's already been initialized because it was accessed above. LazyInitialized.EnsureInitialized should be very cheap, but it can't be any cheaper than just going to the field directly. (We could even cache it in a local, but for the purposes of this change, I think it's best to minimize what's touched to the minimal necessary.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stephentoub Got it! I used a safe programming approach, but true, it is overhead. Will change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're concerned about it, I'd be OK with it stored into a local on first access and then using the local.
if (attrs != null) | ||
{ | ||
return attrs; | ||
} | ||
|
||
lock (s_internalSyncObject) | ||
{ | ||
attrs = (Attribute[])s_attributeCache[type]; | ||
attrs = (Attribute[])AttributeCache[type]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
if (attrs != null) | ||
{ | ||
return attrs; | ||
} | ||
|
||
lock (s_internalSyncObject) | ||
{ | ||
attrs = (Attribute[])s_attributeCache[member]; | ||
attrs = (Attribute[])AttributeCache[member]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
if (events != null) | ||
{ | ||
return events; | ||
} | ||
|
||
lock (s_internalSyncObject) | ||
{ | ||
events = (EventDescriptor[])s_eventCache[type]; | ||
events = (EventDescriptor[])EventCache[type]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
if (extendedProperties == null) | ||
{ | ||
lock (s_internalSyncObject) | ||
{ | ||
extendedProperties = (ReflectPropertyDescriptor[])s_extendedPropertyCache[providerType]; | ||
extendedProperties = (ReflectPropertyDescriptor[])ExtendedPropertyCache[providerType]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
if (properties != null) | ||
{ | ||
return properties; | ||
} | ||
|
||
lock (s_internalSyncObject) | ||
{ | ||
properties = (PropertyDescriptor[])s_propertyCache[type]; | ||
properties = (PropertyDescriptor[])PropertyCache[type]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more minor comments, otherwise LGTM. Thanks.
[s_intrinsicNullableKey] = typeof(NullableConverter), | ||
}; | ||
} | ||
private static Hashtable EditorTables => LazyInitializer.EnsureInitialized(ref s_editorTables, () => new Hashtable(4)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stephentoub Sorry for asking too many questions. I wanted to understand how EnsureInitialized works and found its implementation in the coreclr repo. For the above overload, the implementation is at this link.
It uses Volatile.Read
to load address of target (s_editorTables
in this case). I think the Volatile.Read would introduce memory barrier? The ask in current task is to eliminate memory barriers which are a consequence of volatile fields. But then EnsureInitialized
appears to rely on similar strategy involving volatile reads... Which appears to be contradictory to what we are trying to achieve.
Whereas, the Lazy<T>.Value
implementation (link) appears to directly return the value once initialised under a lock.
I might be missing something, but I thought I would check with you. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The (new) alternative Lazy<T>.Value
is here where _state
is a volatile member.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ask in current task is to eliminate memory barriers which are a consequence of volatile fields.
That's not the goal. The goal is to ensure that lazy initialization is done correctly, safely, and consistently, while minimizing the code required to do so.
But then EnsureInitialized appears to rely on similar strategy involving volatile reads
Any thread-safe lazy initialization strategy is going to rely on volatile reads. That Lazy<T>
doesn't appear to be reading _boxed
via a volatile read technically looks like a bug to me. Now, due to the semantics enforced by the runtime, it's likely going to be made volatile by the runtime anyway, such that the difference wouldn't be noticeable, and even then on x32/64 the hardware enforces a strong memory model such that it further wouldn't be noticeable, but it's still good to do the right thing in the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stephentoub Thanks for the clarification. Actually I got obsessed :( with removal of volatiles due to my interpretation of, or rather being overwhelmed by, the quote in this comment in issue #3862, which I took as necessary for ARM while introducing lazy init. And I got stuck with it... I am good now.
Thanks @manofstick .
@stephentoub , @karelz , @omariom, @manofstick Appreciate your inputs. I am not so well versed in the C# compiler internals / optimization and JIT. So thought I would rather seek advice through discussion before doing any more changes. Thanks in advance. |
Hmm. I will have to defer to the real experts here. My possibly naive understanding is that volatile fields are expensive to write to on ARM (cache flush which could be 100s cycles?) But reads were relatively cheap (just don't allow the optimiser to do any reordering). The existing Lazy implementation does a cast from a object wrapper on each read. My belief is that that is more expensive than the volatile read. From testing on Intel architecture it definitely is. I don't have any ARM machines to play with. |
No, using a volatile read is correct.
To ensure read reorderings/introductions/etc. aren't allowed. While the current runtime / hardware likely prevent such things, the ECMA C# spec would allow such reorderings in a way that could cause problems for lazy init, hence why the lazy constructs use it. |
They are not that expensive on any recent (last 5 years?) ARM processors. .NET Core on ARM is actually using a lot of implicit memory barriers to guarantee type safety. For example: class Test
{
Object _value;
// _value = value has implicit memory barrier on ARM in the current implementation of .NET Core
public SetValue(object value) { _value = value; }
} Assignments of object references have extra overhead anyway because of the extra tracking for GC. The memory barrier makes this overhead a bit higher, but not prohibitively more. The relevant code is here: https://github.com/dotnet/coreclr/blob/master/src/vm/arm/asmhelpers.S#L1242 |
…T> for Singletons Minor edits to directly access the field instead of property once the latter has been called once resulting into lazy initialization.
Would be great if JIT could use ARMv8's |
private static volatile WeakHashtable s_associationTable; | ||
|
||
// A table of type -> default provider to track DefaultTypeDescriptionProviderAttributes. | ||
private static readonly Lazy<Hashtable> s_defaultProvidersLazy = new Lazy<Collections.Hashtable>(() => new Hashtable()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just noticed that in the original code, s_defaultProviders is initialized at its declaration site, so it's not actually lazy. We shouldn't wrap it in Lazy then. The only thing we should do is change the volatile to readonly and remove the code elsewhere that checks whether it could be null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it was null-checked here and I gave it more importance than init at declaration. I will revert and make it readonly.
|
||
if (associations == null) | ||
{ | ||
lock (s_associationTable) | ||
lock (s_associationTableLazy) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is changing the semantic, in that previously it was locking on the table, now it's locking on the lazy. Is there code elsewhere that was expecting all locking to be done on the table itself? Let's keep it the way it was, e.g. lock (s_associationTableLazy.Value) rather than lock (s_associationTableLazy)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the lines of s_defaultProviders
reversal discussion above, does s_associationTable
really require lazy initialisation. The initialisation is just object creation and does not do any heavy code that may cause delays. So how about making it readonly and initialize in-place?
Extending a bit further, do we want to say that lazy initialisation using Lazy<T>
or LazyInitializer.EnsureInitialized
makes more sense if the initialisation is non-trivial. (E.g. s_ProviderTable and s_ProvideTypeTable) in this same file which I left untouched. s_associationTable could also be treated in same way....?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the lines of s_defaultProviders reversal discussion above, does s_associationTable really require lazy initialisation. The initialisation is just object creation and does not do any heavy code that may cause delays. So how about making it readonly and initialize in-place?
I would like to avoid changing lazy/prompt semantics in this PR. The original developer likely had a reason for what they did (lazy vs non-lazy), and so as part of this change I would like to keep it that way. If subsequently you'd like to investigate with appropriate performance tests whether it would be worth changing, that'd be fine as a separate, isolated thing to explore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. I understand the subtlety.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks
…T> for Singletons Made changes as per review comments to use s_defaultProviders as defined originally and to lock on s_associationTableLazy.Value instead of s_associationTableLazy object to preserve semantics.
@karelz, @stephentoub has approved this PR, pending some corrections that I have done just now. I noticed commits are automatically added to existing open PR and so should newer commits (for newly edited files)...? So, I am not sure how to get them into different (multiple small) PRs as we discussed in issue #3862... |
private static volatile Hashtable s_defaultProviders = new Hashtable(); // A table of type -> default provider to track DefaultTypeDescriptionProviderAttributes. | ||
private static volatile WeakHashtable s_associationTable; | ||
private static readonly Hashtable s_defaultProviders = new Hashtable(); // A table of type -> default provider to track DefaultTypeDescriptionProviderAttributes. | ||
private static readonly Lazy<WeakHashtable> s_associationTableLazy = new Lazy<WeakHashtable>(() => new WeakHashtable()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Last change I think:
Sorry if I mislead you on the last round of reviews, but it looks like WeakHashtable just derives from Hashtable and adds a few small fields... probably better to make this one LazyInitializer instead of a readonly Lazy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. I'm getting a lot to learn. Thanks for support.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this. One final comment, then looks good to merge.
@WinCPP did you try to use different branch for each PR? |
@WinCPP could yo please fix the code according to last comment from @stephentoub so we can merge the changes? Thanks. |
@tarekgh yup working on it. Please give me another hour... I should have changes in. |
…T> for Singletons Made changes as per review comments to use LazyInitializer for s_associationTable.
Thanks, @WinCPP. |
…atic or Lazy<T> for Singletons (dotnet/corefx#16216) * Fix dotnet/corefx#3862 - Double-Check locked patterns and consider static or Lazy<T> for Singletons Commit migrated from dotnet/corefx@3e7fb77
This is first of the few PRs for the changes to replace double-check locked patterns with static or Lazy constructs.