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

Improve data contract serializers performance in dictionary scenario #7608

Merged

Conversation

khdang
Copy link
Member

@khdang khdang commented Apr 8, 2016

  • Improve data contract serializers performance in dictionary scenario by 2x.
  • Instead of the current way to get encoded string which involves string creation and buffer copy, use the encoding.GetBytes API
  • When creating a new instance of a type without a default constructor we use reflection to invoke a method that is not from the serialization assembly. Since that is significantly slower, we should use Activator.CreateInstance whenever we can. The Activator.CreateInstance throws exception so we need to check to know if a type has default constructor or not and use the appropriate method. As this check is also slow, so I've added a cache to make sure we only do this once for each type.
  • Internally dictionaries are handled as collection of KeyValues. I've updated this type to have default constructor so we can use Activator.CreateInstance to create new instance of it.

Update:

  • Instead of converting KeyValue type to class, which potentially causes GC long pauses, use Activator.CreateInstance for value type

cc: @shmao @SGuyGe @zhenlan @mconnew

Fix #7578 and #7577

@@ -68,14 +68,16 @@ public T Value

[DataContract(Namespace = "http://schemas.microsoft.com/2003/10/Serialization/Arrays")]
#if USE_REFEMIT
public struct KeyValue<K, V>
public class KeyValue<K, V>
#else
Copy link
Member

Choose a reason for hiding this comment

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

why a change from struct? (not saying it's wrong, I just don't follow)

Copy link
Member Author

Choose a reason for hiding this comment

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

The current issue with dictionary is that they are handled as collection of KeyValue internally and as of now, KeyValue don't have default constructor so they're not created by the faster code path. I added a default constructor but since struct is not allowed to have explicit parameterless constructor, I've changed this type to class.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm concerned on this as it diverges from desktop .NET. We'd better avoid it unless absolutely necessary. How much performance gain would this change give us?

Copy link
Member Author

Choose a reason for hiding this comment

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

This change and the caching of the map indicate type has a default constructor or not give 1.75x improvement for the dictionary scenario. Any specific concern on this change?

Copy link
Contributor

Choose a reason for hiding this comment

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

What's the perf gain without changing from struct to class?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is part of the significant improvement in this PR and benefits mainly the dictionary scenario. Other are the change to use encoding.GetBytes which improve just a few percentage, and caching instance of the GetUninitializedObject to avoid repeated reflection which already improves a lot, I recall close to 50%. With this change the performance is at the same bar or better than Desktop in dictionary.

Copy link
Member

Choose a reason for hiding this comment

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

I'm concerned on this as it diverges from desktop .NET.

If the change proves to be beneficial to desktop, we should port the fix to desktop too.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree with @zhenlan. I'm curious about the perf difference between desktop and .NET core after this change were made on both. There may be some hotspots not yet identified. LGTM.

Copy link
Member Author

Choose a reason for hiding this comment

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

I forgot to mention that this change is for NetCore only since this is the part where implementation on Desktop and NetCore diverges. In Desktop we use RuntimeServices.GetUninitlaizedObject to create new object instance whereas in NetCore that API is not available. We opt for getting that method through reflection and this perf issue is incurred.

@khdang khdang force-pushed the improve_serialization_perf_dcs_dictionary branch from 8e670d4 to 773af28 Compare April 9, 2016 05:33
@khdang
Copy link
Member Author

khdang commented Apr 9, 2016

Test Innerloop Windows_NT Debug Build and Test please

@khdang
Copy link
Member Author

khdang commented Apr 10, 2016

@dotnet-bot test this please

@khdang
Copy link
Member Author

khdang commented Apr 10, 2016

@dotnet-bot test Innerloop Windows_NT Debug Build and Test please

@@ -30,6 +30,19 @@ public sealed class XmlFormatReaderGenerator
internal sealed class XmlFormatReaderGenerator
#endif
{
private static MethodInfo _getUninitializedObjectMethodInfo;
Copy link
Contributor

Choose a reason for hiding this comment

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

s_getUninitializedObjectMethodInfo?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes that's the right name for private static.

@khdang khdang force-pushed the improve_serialization_perf_dcs_dictionary branch from 773af28 to 042ed27 Compare April 12, 2016 17:53
#else
internal struct KeyValue<K, V>
Copy link
Member

Choose a reason for hiding this comment

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

Changing KeyValue from a struct to a class will have a significant performance hit which you won't see in a micro benchmark. This could place a lot of stress on the heap and cause long GC pauses some time later. It will could also cause significantly slower memory accesses as an array of structs are next to each other in memory. An array of classes will require an extra dereference which will be slower, and you won't benefit from cache locality and on high core machines under sufficient load to context switch, this could be a significant issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. I've reverted the change of KeyValue back to struct to avoid potential GC impact. I tried a few alternatives to this and realized that Activator.CreateInstance on a struct type uses a special IL for struct and so just calling Activator.CreateInstance will work. I get the numbers for this change and they are as good as the previous change for dictionary scenario.

@khdang
Copy link
Member Author

khdang commented Apr 14, 2016

I've updated the PR to address comments on GC and formatting. Can you take a look?
cc: @shmao @mconnew

if (type.GetTypeInfo().IsValueType)
{
return Activator.CreateInstance(type);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: New line after }

@shmao
Copy link
Contributor

shmao commented Apr 15, 2016

:shipit:

@khdang khdang force-pushed the improve_serialization_perf_dcs_dictionary branch from 243289a to 905308b Compare April 15, 2016 20:51
@mconnew
Copy link
Member

mconnew commented Apr 16, 2016

:shipit:

private static MethodInfo s_getUninitializedObjectMethodInfo;
private static Dictionary<Type, bool> s_typeHasDefaultConstructorMap;

static XmlFormatReaderGenerator()
Copy link
Contributor

@justinvp justinvp Apr 16, 2016

Choose a reason for hiding this comment

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

This explicit static cctor causes the C# compiler to not mark the type as beforefieldinit, which in turn means that the backend compiler needs to add checks to static methods to ensure that the type has been initialized. See #6016 for details.

To avoid this, consider removing the static cctor and initializing the static fields directly, e.g.:

private static readonly MethodInfo s_getUninitializedObjectMethodInfo =
    typeof(string)
    .GetTypeInfo()
    .Assembly
    .GetType("System.Runtime.Serialization.FormatterServices")
    ?.GetMethod("GetUninitializedObject", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);

private static readonly Dictionary<Type, bool> s_typeHasDefaultConstructorMap = new Dictionary<Type, bool>();

Also, these static fields can be readonly.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for pointing this out! I have removed the static constructor.

@khdang khdang force-pushed the improve_serialization_perf_dcs_dictionary branch from 905308b to 91df870 Compare April 18, 2016 18:42
@khdang
Copy link
Member Author

khdang commented Apr 18, 2016

@dotnet-bot test this please

@khdang
Copy link
Member Author

khdang commented Apr 18, 2016

@dotnet-bot test Innerloop Ubuntu Release Build and Test please

@khdang
Copy link
Member Author

khdang commented Apr 18, 2016

@dotnet-bot test Innerloop OSX Debug Build and Test please
@dotnet-bot test Innerloop Ubuntu Release Build and Test please

@mconnew
Copy link
Member

mconnew commented Apr 19, 2016

Did you try replacing the MethodInfo.Invoke call with creating a delegate and calling that instead?

@khdang
Copy link
Member Author

khdang commented Apr 19, 2016

Yes I did. I was experimenting to get the perf numbers for different changes and wasn't sure how to add that change. I've now updated to include it as well.

@@ -30,6 +30,17 @@ public sealed class XmlFormatReaderGenerator
internal sealed class XmlFormatReaderGenerator
#endif
{
private static readonly MethodInfo s_getUninitializedObjectMethodInfo =
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this field be removed now, replaced by the delegate field?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes it can. I was thinking the line may get too long but actually it looks Ok.

@khdang khdang force-pushed the improve_serialization_perf_dcs_dictionary branch from 25e8738 to fcc71d2 Compare April 19, 2016 20:06
{
obj = methodInfo.Invoke(null, new object[] { type });
}
obj = s_getUninitializedObjectDelegate(type);
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry for not pointing this out earlier, but this whole method could be simplified as:

static internal object TryGetUninitializedObjectWithFormatterServices(Type type) =>
    s_getUninitializedObjectDelegate?.Invoke(type);

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice. Updated as suggestion.

@khdang khdang force-pushed the improve_serialization_perf_dcs_dictionary branch from fcc71d2 to 025779f Compare April 19, 2016 22:43
@khdang
Copy link
Member Author

khdang commented Apr 21, 2016

Test Innerloop CentOS7.1 Debug Build and Test please

@stephentoub
Copy link
Member

Test Innerloop OSX Debug Build and Test please
Test Innerloop OSX Release Build and Test please

@khdang khdang merged commit 0444706 into dotnet:master Apr 22, 2016
@khdang khdang deleted the improve_serialization_perf_dcs_dictionary branch April 25, 2016 17:49
@karelz karelz modified the milestone: 1.0.0-rtm Dec 3, 2016
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…on_perf_dcs_dictionary

Improve data contract serializers performance in dictionary scenario

Commit migrated from dotnet/corefx@0444706
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.