Skip to content

Commit 1bf2d74

Browse files
committed
Optimize default value lookups (20% faster)
| Method | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio | |------------------------ |---------:|---------:|---------:|------:|----------:|------------:| | ActivatorCreateInstance | 84.13 ms | 4.029 ms | 1.046 ms | 1.00 | 69 B | 1.00 | | LookupCached | 67.93 ms | 0.345 ms | 0.053 ms | 0.81 | 7 B | 0.10 | ```c# using System.Collections.Concurrent; using BenchmarkDotNet.Attributes; namespace Benchmarks; // ReSharper disable once ClassCanBeSealed.Global [MarkdownExporter] [SimpleJob(1, 5, 5)] [MemoryDiagnoser] public class DefaultValueBenchmarks { private const int IterationCount = 10_000_000; private static readonly ConcurrentDictionary<Type, object?> Cache = new() { [typeof(int?)] = null, [typeof(Guid?)] = null }; [Benchmark(Baseline = true)] public void ActivatorCreateInstance() { for (int index = 0; index < IterationCount; index++) { _ = Activator.CreateInstance(typeof(int?)); _ = Activator.CreateInstance(typeof(Guid?)); } } [Benchmark] public void LookupCached() { for (int index = 0; index < IterationCount; index++) { _ = Cache.TryGetValue(typeof(int?), out _); _ = Cache.TryGetValue(typeof(Guid?), out _); } } } ```
1 parent 7bda8a8 commit 1bf2d74

File tree

3 files changed

+13
-26
lines changed

3 files changed

+13
-26
lines changed

src/Examples/DapperExample/Repositories/ResultSetMapper.cs

+1-16
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ internal sealed class ResultSetMapper<TResource, TId>
1818
// Note we don't do full bidirectional relationship fix-up; this just avoids duplicate instances.
1919
private readonly Dictionary<Type, Dictionary<object, object>> _resourceByTypeCache = [];
2020

21-
// Optimization to avoid unneeded calls to expensive Activator.CreateInstance() method, which is needed multiple times per row.
22-
private readonly Dictionary<Type, object?> _defaultValueByTypeCache = [];
23-
2421
// Used to determine where in the tree of included relationships a join object belongs to.
2522
private readonly Dictionary<IncludeElementExpression, int> _includeElementToJoinObjectArrayIndexLookup = new(ReferenceEqualityComparer.Instance);
2623

@@ -114,22 +111,10 @@ public ResultSetMapper(IncludeExpression? include)
114111

115112
private bool HasDefaultValue(object value)
116113
{
117-
object? defaultValue = GetDefaultValueCached(value.GetType());
114+
object? defaultValue = RuntimeTypeConverter.GetDefaultValue(value.GetType());
118115
return Equals(defaultValue, value);
119116
}
120117

121-
private object? GetDefaultValueCached(Type type)
122-
{
123-
if (_defaultValueByTypeCache.TryGetValue(type, out object? defaultValue))
124-
{
125-
return defaultValue;
126-
}
127-
128-
defaultValue = RuntimeTypeConverter.GetDefaultValue(type);
129-
_defaultValueByTypeCache[type] = defaultValue;
130-
return defaultValue;
131-
}
132-
133118
private void RecursiveSetRelationships(object leftResource, IEnumerable<IncludeElementExpression> includeElements, object?[] joinObjects)
134119
{
135120
foreach (IncludeElementExpression includeElement in includeElements)

src/JsonApiDotNetCore.Annotations/Resources/RuntimeTypeConverter.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Concurrent;
12
using System.Globalization;
23
using JetBrains.Annotations;
34

@@ -13,6 +14,8 @@ public static class RuntimeTypeConverter
1314
{
1415
private const string ParseQueryStringsUsingCurrentCultureSwitchName = "JsonApiDotNetCore.ParseQueryStringsUsingCurrentCulture";
1516

17+
private static readonly ConcurrentDictionary<Type, object?> DefaultTypeCache = new();
18+
1619
/// <summary>
1720
/// Converts the specified value to the specified type.
1821
/// </summary>
@@ -137,6 +140,8 @@ public static class RuntimeTypeConverter
137140
/// </summary>
138141
public static bool CanContainNull(Type type)
139142
{
143+
ArgumentGuard.NotNull(type);
144+
140145
return !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
141146
}
142147

@@ -148,6 +153,8 @@ public static bool CanContainNull(Type type)
148153
/// </returns>
149154
public static object? GetDefaultValue(Type type)
150155
{
151-
return type.IsValueType ? Activator.CreateInstance(type) : null;
156+
ArgumentGuard.NotNull(type);
157+
158+
return type.IsValueType ? DefaultTypeCache.GetOrAdd(type, Activator.CreateInstance) : null;
152159
}
153160
}

src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs

+4-9
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,12 @@ public static object GetTypedId(this IIdentifiable identifiable)
1818
}
1919

2020
object? propertyValue = property.GetValue(identifiable);
21+
object? defaultValue = RuntimeTypeConverter.GetDefaultValue(property.PropertyType);
2122

22-
// PERF: We want to throw when 'Id' is unassigned without doing an expensive reflection call, unless this is likely the case.
23-
if (identifiable.StringId == null)
23+
if (Equals(propertyValue, defaultValue))
2424
{
25-
object? defaultValue = RuntimeTypeConverter.GetDefaultValue(property.PropertyType);
26-
27-
if (Equals(propertyValue, defaultValue))
28-
{
29-
throw new InvalidOperationException($"Property '{identifiable.GetClrType().Name}.{IdPropertyName}' should " +
30-
$"have been assigned at this point, but it contains its default {property.PropertyType.Name} value '{propertyValue}'.");
31-
}
25+
throw new InvalidOperationException($"Property '{identifiable.GetClrType().Name}.{IdPropertyName}' should " +
26+
$"have been assigned at this point, but it contains its default {property.PropertyType.Name} value '{propertyValue}'.");
3227
}
3328

3429
return propertyValue!;

0 commit comments

Comments
 (0)