Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Fix #3862 - Double-Check locked patterns and consider static or Lazy<T> for Singletons #16216

Merged
merged 15 commits into from
Feb 24, 2017

Conversation

WinCPP
Copy link

@WinCPP WinCPP commented Feb 16, 2017

This is first of the few PRs for the changes to replace double-check locked patterns with static or Lazy constructs.


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());
Copy link
Member

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());
Copy link
Member

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.

Copy link
Author

@WinCPP WinCPP Feb 17, 2017

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,

  1. 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).
  2. 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]

Copy link
Author

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

Copy link
Member

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();
Copy link
Member

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?

Copy link
Author

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];
Copy link
Member

@stephentoub stephentoub Feb 18, 2017

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

Copy link
Author

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.

Copy link
Member

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];
Copy link
Member

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];
Copy link
Member

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];
Copy link
Member

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];
Copy link
Member

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];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

Copy link
Member

@stephentoub stephentoub left a 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));
Copy link
Author

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.

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Author

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.

@manofstick
Copy link
Contributor

@WinCPP

I doubt it changes any of your thinking but Lazy is (probably) getting a revamp under this PR.

@WinCPP WinCPP changed the title Fix #3862 - Double-Check locked patterns and consider static or Lazy<T> for Singletons Fix #3862 - [Do not merge] Double-Check locked patterns and consider static or Lazy<T> for Singletons Feb 19, 2017
@WinCPP
Copy link
Author

WinCPP commented Feb 19, 2017

Thanks @manofstick .

  1. Do we see a conflict here between new Lazy<T> behavior with the core of the issue #3862 vis-a-vis volatile variables / reads?
  2. The implementation (here) of the overload EnsureInitialized(ref T target, Func<T> valueFactory) (MSDN) uses Volatile.Read, which I am unable to understand in context of the current issue and also through implementation point of view. From documentation, I thought, except for the lock based overload of EnsureInitialized (here), the other overloads would allow multiple threads to initialize the object but only one of them would be CompareExchange'd as actual value with init'd objects being left for garbage collection; and hence I wonder why do we require the Volatile.Read.

@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.

@manofstick
Copy link
Contributor

@WinCPP

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.

@stephentoub
Copy link
Member

Do we see a conflict here between new Lazy behavior with the core of the issue #3862 vis-a-vis volatile variables / reads?

No, using a volatile read is correct.

hence I wonder why do we require the Volatile.Read

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.

@WinCPP WinCPP changed the title Fix #3862 - [Do not merge] Double-Check locked patterns and consider static or Lazy<T> for Singletons Fix #3862 - Double-Check locked patterns and consider static or Lazy<T> for Singletons Feb 20, 2017
@jkotas
Copy link
Member

jkotas commented Feb 20, 2017

possibly naive understanding is that volatile fields are expensive to write to on ARM

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.
@omariom
Copy link
Contributor

omariom commented Feb 21, 2017

@jkotas

They are not that expensive on any recent (last 5 years?) ARM processors.

Would be great if JIT could use ARMv8's ldar and stlr instead of dmbs.

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());
Copy link
Member

@stephentoub stephentoub Feb 21, 2017

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.

Copy link
Author

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)
Copy link
Member

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)

Copy link
Author

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....?

Copy link
Member

@stephentoub stephentoub Feb 21, 2017

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.

Copy link
Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

WinCPP and others added 3 commits February 24, 2017 00:15
…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.
@WinCPP
Copy link
Author

WinCPP commented Feb 23, 2017

@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());
Copy link
Member

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.

Copy link
Author

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.

Copy link
Member

@stephentoub stephentoub left a 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.

@karelz
Copy link
Member

karelz commented Feb 23, 2017

So, I am not sure how to get them into different (multiple small) PRs as we discussed in issue #3862...

@WinCPP did you try to use different branch for each PR?

@tarekgh
Copy link
Member

tarekgh commented Feb 24, 2017

@WinCPP could yo please fix the code according to last comment from @stephentoub so we can merge the changes? Thanks.

@WinCPP
Copy link
Author

WinCPP commented Feb 24, 2017

@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.
@stephentoub
Copy link
Member

Thanks, @WinCPP.

@stephentoub stephentoub merged commit 3e7fb77 into dotnet:master Feb 24, 2017
@karelz karelz modified the milestone: 2.0.0 Mar 1, 2017
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…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
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants