[CSHARP-2556] Reduce memory allocations in BsonSerializerRegistry.GetSerializer() Created: 20/Mar/19  Updated: 08/Jun/23

Status: Backlog
Project: C# Driver
Component/s: Performance, Serialization
Affects Version/s: 2.8.0
Fix Version/s: None

Type: Improvement Priority: Major - P3
Reporter: Daniel Hegener Assignee: Unassigned
Resolution: Unresolved Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Attachments: PNG File image-2019-03-20-10-03-01-856.png    
Issue Links:
Related
Backwards Compatibility: Fully Compatible

 Description   

The BsonSerializerRegistry.GetSerializer() gets called repeatedly on the hot path during serialization/deserialization. It allocates memory due to an implicit closure of the current instance variable ("this").

This allocation can be avoided.

For the versions of the .NET frameworks which we are dealing with (.NET 4.5.2/.NET Standard 1.5) the following approach would work: https://github.com/dotnet/corefx/issues/394

From .NET 4.7.2 onwards it is possible to simply switch to a special overload of the ConcurrentDictionary.GetOrAdd() method which accepts a factory argument as described here https://github.com/dotnet/corefx/pull/1783 and documented here (https://docs.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2.getoradd?view=netframework-4.7.2#System_Collections_Concurrent_ConcurrentDictionary_2_GetOrAdd__1__0_System_Func__0___0__1____0_)



 Comments   
Comment by Daniel Hegener [ 20/Jun/19 ]

I ran some basic benchmark (https://gist.github.com/dnickless/7dcd4f8d488c791a478f76e9acba4be2) using various configurations and a .NET 4.7.2 based implementation where I also moved the reflection based checks inside the factory:

 

 

        public IBsonSerializer GetSerializer(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }
            return _cache.GetOrAdd(type, (typeInternal, bsonSerializerRegistry) =>
            {
                var typeInfo = typeInternal.GetTypeInfo();
                if (typeInfo.IsGenericType && typeInfo.ContainsGenericParameters)
                {
                    var message = string.Format("Generic type {0} has unassigned type parameters.", BsonUtils.GetFriendlyTypeName(typeInternal));
                    throw new ArgumentException(message, "type");
                }
                return bsonSerializerRegistry.CreateSerializer(typeInternal);
            }, this);
        }

Here are the results:

WITHOUT changes, original version:

Method Jit Platform Runtime Mean Error StdDev
SingleThreaded LegacyJit X64 Clr 4.610 ms 0.0909 ms 0.1360 ms
MultiThreaded LegacyJit X64 Clr 7,936.540 ms 187.6947 ms 175.5697 ms
SingleThreaded LegacyJit X86 Clr 4.548 ms 0.0909 ms 0.1639 ms
MultiThreaded LegacyJit X86 Clr 7,448.053 ms 99.8361 ms 88.5021 ms
SingleThreaded RyuJit X64 Clr 4.251 ms 0.0932 ms 0.1245 ms
MultiThreaded RyuJit X64 Clr 7,451.868 ms 50.9869 ms 47.6932 ms

WITH changes

Method Jit Platform Runtime Mean Error StdDev
SingleThreaded LegacyJit X64 Clr 4.410 ms 0.0975 ms 0.2876 ms
MultiThreaded LegacyJit X64 Clr 7,140.595 ms 81.4239 ms 76.1640 ms
SingleThreaded LegacyJit X86 Clr 4.292 ms 0.0852 ms 0.0756 ms
MultiThreaded LegacyJit X86 Clr 7,267.805 ms 72.3405 ms 67.6674 ms
SingleThreaded RyuJit X64 Clr 4.034 ms 0.0834 ms 0.1949 ms
MultiThreaded RyuJit X64 Clr 6,707.119 ms 94.2157 ms 88.1295 ms
Comment by Ian Whalen (Inactive) [ 25/Mar/19 ]

dnickless thanks for the report - do you have any idea of the perf impact of the proposed changes here? any rough tests you can share, etc?

Generated at Wed Feb 07 21:42:53 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.