Skip to content

Commit c0e0c0e

Browse files
authored
Feature/improve allocations (#49)
* Improve memory allocation thanks to expression trees * Update benchmark results * Add benchmark for getters * Native benchmark more accurate * Improve naming
1 parent 481d4e8 commit c0e0c0e

10 files changed

Lines changed: 256 additions & 65 deletions

File tree

images/bench-mysql.png

50 Bytes
Loading

images/bench-postgresql.png

-296 Bytes
Loading

images/bench-sqlite.png

137 Bytes
Loading

images/bench-sqlserver.png

282 Bytes
Loading

src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
using Microsoft.EntityFrameworkCore;
22
using Microsoft.EntityFrameworkCore.Metadata;
33

4-
using PhenX.EntityFrameworkCore.BulkInsert.Abstractions;
54
using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
65
using PhenX.EntityFrameworkCore.BulkInsert.Options;
76

87
namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata;
98

109
internal sealed class ColumnMetadata(IProperty property, SqlDialectBuilder dialect)
1110
{
12-
private readonly PropertyAccessor.Getter<object, object?> _getter = BuildGetter(property);
11+
private readonly Func<object, object?> _getter = BuildGetter(property);
1312

1413
public IProperty Property { get; } = property;
1514

@@ -44,28 +43,15 @@ internal sealed class ColumnMetadata(IProperty property, SqlDialectBuilder dial
4443
return result;
4544
}
4645

47-
private static PropertyAccessor.Getter<object, object?> BuildGetter(IProperty property)
46+
private static Func<object, object?> BuildGetter(IProperty property)
4847
{
4948
var valueConverter =
5049
property.GetValueConverter() ??
5150
property.GetTypeMapping().Converter;
5251

5352
var propInfo = property.PropertyInfo!;
5453

55-
var actualGetter =
56-
PropertyAccessor.CreateUntypedGetter(
57-
propInfo,
58-
property.DeclaringType.ClrType,
59-
property.ClrType);
60-
61-
if (valueConverter == null)
62-
{
63-
return actualGetter;
64-
}
65-
66-
var converter = valueConverter.ConvertToProvider;
67-
68-
return source => converter(actualGetter(source));
54+
return PropertyAccessor.CreateGetter(propInfo, valueConverter?.ConvertToProviderExpression);
6955
}
7056

7157
private static string GetStoreDefinition(IProperty property)
Lines changed: 35 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,53 @@
1-
using System.Reflection.Emit;
1+
using System.Linq.Expressions;
22
using System.Reflection;
33

44
namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata;
55

66
internal static class PropertyAccessor
77
{
8-
public delegate TValue Getter<TSource, TValue>(TSource source);
9-
10-
public static Getter<object, object?> CreateUntypedGetter(PropertyInfo propertyInfo, Type sourceType, Type valueType)
8+
public static Func<object, object?> CreateGetter(PropertyInfo propertyInfo, LambdaExpression? converter = null)
119
{
12-
var method =
13-
typeof(PropertyAccessor).GetMethod(nameof(CreateInternalUntypedGetter), BindingFlags.NonPublic | BindingFlags.Static)!
14-
.MakeGenericMethod(sourceType, valueType);
15-
16-
return (Getter<object, object?>)method.Invoke(null, [propertyInfo])!;
17-
}
10+
ArgumentNullException.ThrowIfNull(propertyInfo);
11+
var getMethod = propertyInfo.GetMethod ?? throw new ArgumentException("Property does not have a getter.");
1812

19-
private static Getter<object, object?> CreateInternalUntypedGetter<TSource, TValue>(PropertyInfo propertyInfo)
20-
{
21-
var getter = CreateGetter<TSource, TValue>(propertyInfo);
13+
var instanceParam = Expression.Parameter(typeof(object), "instance");
2214

23-
return source => getter((TSource)source!);
24-
}
15+
// Convert object to the declaring type
16+
Expression typedInstance = propertyInfo.DeclaringType!.IsValueType
17+
? Expression.Unbox(instanceParam, propertyInfo.DeclaringType)
18+
: Expression.Convert(instanceParam, propertyInfo.DeclaringType);
2519

26-
public static Getter<TSource, TValue> CreateGetter<TSource, TValue>(PropertyInfo propertyInfo)
27-
{
28-
if (!propertyInfo.CanRead)
29-
{
30-
return x => throw new NotSupportedException();
31-
}
20+
// Call Getter
21+
Expression getterExpression = Expression.Call(typedInstance, getMethod);
3222

33-
var bakingField =
34-
propertyInfo.DeclaringType!.GetField($"<{propertyInfo.Name}>k__BackingField",
35-
BindingFlags.NonPublic |
36-
BindingFlags.Instance);
23+
var propertyType = propertyInfo.PropertyType;
3724

38-
var propertyGetMethod = propertyInfo.GetGetMethod()!;
39-
40-
var getMethod = new DynamicMethod(propertyGetMethod.Name, typeof(TValue), [typeof(TSource)], true);
41-
var getGenerator = getMethod.GetILGenerator();
42-
43-
// Load this to stack.
44-
getGenerator.Emit(OpCodes.Ldarg_0);
45-
46-
if (bakingField != null && !propertyGetMethod.IsVirtual)
47-
{
48-
// Get field directly.
49-
getGenerator.Emit(OpCodes.Ldfld, bakingField);
50-
}
51-
else if (propertyGetMethod.IsVirtual)
52-
{
53-
// Call the virtual property.
54-
getGenerator.Emit(OpCodes.Callvirt, propertyGetMethod);
55-
}
56-
else
25+
// If the converter is provided, we call it
26+
if (converter != null)
5727
{
58-
// Call the non virtual property.
59-
getGenerator.Emit(OpCodes.Call, propertyGetMethod);
28+
// Validate the converter input type matches property type
29+
var converterParamType = converter.Parameters[0].Type;
30+
if (!converterParamType.IsAssignableFrom(propertyType) && !propertyType.IsAssignableFrom(converterParamType))
31+
{
32+
throw new ArgumentException($"Converter input must be assignable from property type ({propertyType} -> {converterParamType})");
33+
}
34+
35+
// If property type != converter param, convert
36+
var converterInput = getterExpression;
37+
if (converterParamType != propertyType)
38+
{
39+
converterInput = Expression.Convert(getterExpression, converterParamType);
40+
}
41+
42+
getterExpression = Expression.Invoke(converter, converterInput);
43+
44+
propertyType = getterExpression.Type;
6045
}
6146

62-
getGenerator.Emit(OpCodes.Ret);
47+
var finalExpression = propertyType.IsValueType
48+
? Expression.Convert(getterExpression, typeof(object))
49+
: getterExpression;
6350

64-
return getMethod.CreateDelegate<Getter<TSource, TValue>>();
51+
return Expression.Lambda<Func<object, object?>>(finalExpression, instanceParam).Compile();
6552
}
6653
}

src/PhenX.EntityFrameworkCore.BulkInsert/PhenX.EntityFrameworkCore.BulkInsert.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
</ItemGroup>
77

88
<ItemGroup>
9+
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.Benchmark" />
910
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.Tests" />
1011
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.MySql" />
1112
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.PostgreSql" />
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System.Reflection;
2+
using System.Reflection.Emit;
3+
4+
namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark;
5+
6+
public partial class GetValueComparator
7+
{
8+
public static Func<object, object?> CreateUntypedGetter(PropertyInfo propertyInfo, Type sourceType, Type valueType)
9+
{
10+
var method =
11+
typeof(GetValueComparator).GetMethod(nameof(CreateInternalUntypedGetter), BindingFlags.NonPublic | BindingFlags.Static)!
12+
.MakeGenericMethod(sourceType, valueType);
13+
14+
return (Func<object, object?>)method.Invoke(null, [propertyInfo])!;
15+
}
16+
17+
private static Func<object, object?> CreateInternalUntypedGetter<TSource, TValue>(PropertyInfo propertyInfo)
18+
{
19+
var getter = CreateGetter<TSource, TValue>(propertyInfo);
20+
21+
return source => getter((TSource)source!);
22+
}
23+
24+
public static Func<TSource, TValue> CreateGetter<TSource, TValue>(PropertyInfo propertyInfo)
25+
{
26+
if (!propertyInfo.CanRead)
27+
{
28+
return x => throw new NotSupportedException();
29+
}
30+
31+
var bakingField =
32+
propertyInfo.DeclaringType!.GetField($"<{propertyInfo.Name}>k__BackingField",
33+
BindingFlags.NonPublic |
34+
BindingFlags.Instance);
35+
36+
var propertyGetMethod = propertyInfo.GetGetMethod()!;
37+
38+
var getMethod = new DynamicMethod(propertyGetMethod.Name, typeof(TValue), [typeof(TSource)], true);
39+
var getGenerator = getMethod.GetILGenerator();
40+
41+
// Load this to stack.
42+
getGenerator.Emit(OpCodes.Ldarg_0);
43+
44+
if (bakingField != null && !propertyGetMethod.IsVirtual)
45+
{
46+
// Get field directly.
47+
getGenerator.Emit(OpCodes.Ldfld, bakingField);
48+
}
49+
else if (propertyGetMethod.IsVirtual)
50+
{
51+
// Call the virtual property.
52+
getGenerator.Emit(OpCodes.Callvirt, propertyGetMethod);
53+
}
54+
else
55+
{
56+
// Call the non virtual property.
57+
getGenerator.Emit(OpCodes.Call, propertyGetMethod);
58+
}
59+
60+
getGenerator.Emit(OpCodes.Ret);
61+
62+
return getMethod.CreateDelegate<Func<TSource, TValue>>();
63+
}
64+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
using System.Linq.Expressions;
2+
using System.Reflection;
3+
4+
using BenchmarkDotNet.Attributes;
5+
using BenchmarkDotNet.Engines;
6+
7+
using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
8+
9+
namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark;
10+
11+
[MemoryDiagnoser]
12+
[SimpleJob(RunStrategy.ColdStart, launchCount: 1, warmupCount: 0, iterationCount: 10)]
13+
public partial class GetValueComparator
14+
{
15+
[Params(1_000_000)] public int N;
16+
17+
private IReadOnlyList<TestEntity> data = [];
18+
19+
[IterationSetup]
20+
public void IterationSetup()
21+
{
22+
data = Enumerable.Range(1, N).Select(i => new TestEntity
23+
{
24+
Name = $"Entity{i}",
25+
Price = (decimal)(i * 0.1),
26+
Identifier = Guid.NewGuid(),
27+
NumericEnumValue = (NumericEnum)(i % 2),
28+
}).ToList();
29+
}
30+
31+
private static Dictionary<string, Expression<Func<object?, object?>>> Converters = new()
32+
{
33+
{ nameof(TestEntity.NumericEnumValue), v => (int) v},
34+
};
35+
36+
private static readonly PropertyInfo[] PropertyInfos = typeof(TestEntity).GetProperties();
37+
38+
private static readonly Func<object, object?>[] PropertyInfoGetValueGetters = PropertyInfos
39+
.Select<PropertyInfo, Func<object, object?>>(propertyInfo =>
40+
{
41+
var converter = Converters.TryGetValue(propertyInfo.Name, out var expression)
42+
? expression.Compile()
43+
: null;
44+
45+
if (converter == null)
46+
{
47+
return propertyInfo.GetValue;
48+
}
49+
50+
return entity => converter(propertyInfo.GetValue(entity));
51+
})
52+
.ToArray();
53+
54+
private static readonly Func<object, object?>[] PropertyInfoIlGetters = PropertyInfos
55+
.Select<PropertyInfo, Func<object, object?>>(propertyInfo =>
56+
{
57+
var converter = Converters.TryGetValue(propertyInfo.Name, out var expression)
58+
? expression.Compile()
59+
: null;
60+
61+
var getter = CreateUntypedGetter(propertyInfo, propertyInfo.DeclaringType!, propertyInfo.PropertyType);
62+
63+
if (converter == null)
64+
{
65+
return getter;
66+
}
67+
68+
return entity => converter(getter(entity));
69+
})
70+
.ToArray();
71+
72+
private static readonly Func<object, object?>[] PropertyAccessorGetters = PropertyInfos
73+
.Select<PropertyInfo, Func<object, object?>>(propertyInfo =>
74+
{
75+
var converter = Converters.TryGetValue(propertyInfo.Name, out var expression)
76+
? expression
77+
: null;
78+
79+
return PropertyAccessor.CreateGetter(propertyInfo, converter);
80+
})
81+
.ToArray();
82+
83+
[Benchmark(Baseline = true)]
84+
public void Native()
85+
{
86+
var enumConverter = Converters[nameof(TestEntity.NumericEnumValue)].Compile();
87+
88+
for (var i = 0; i < data.Count; i++)
89+
{
90+
var entity = data[i];
91+
92+
_ = entity.Id;
93+
_ = entity.Name;
94+
_ = entity.Price;
95+
_ = entity.Identifier;
96+
_ = enumConverter(entity.NumericEnumValue);
97+
_ = entity.CreatedAt;
98+
_ = entity.UpdatedAt;
99+
}
100+
}
101+
102+
[Benchmark]
103+
public void PropertyInfo_GetValue()
104+
{
105+
for (var i = 0; i < data.Count; i++)
106+
{
107+
var entity = data[i];
108+
109+
for (var j = 0; j < PropertyInfoGetValueGetters.Length; j++)
110+
{
111+
var getter = PropertyInfoGetValueGetters[j];
112+
113+
_ = getter(entity);
114+
}
115+
}
116+
}
117+
118+
[Benchmark]
119+
public void ExpressionTreeGetter()
120+
{
121+
for (var i = 0; i < data.Count; i++)
122+
{
123+
var entity = data[i];
124+
125+
for (var j = 0; j < PropertyAccessorGetters.Length; j++)
126+
{
127+
var getter = PropertyAccessorGetters[j];
128+
129+
_ = getter(entity);
130+
}
131+
}
132+
}
133+
134+
[Benchmark]
135+
public void IlGetter()
136+
{
137+
for (var i = 0; i < data.Count; i++)
138+
{
139+
var entity = data[i];
140+
141+
for (var j = 0; j < PropertyInfoIlGetters.Length; j++)
142+
{
143+
var getter = PropertyInfoIlGetters[j];
144+
145+
_ = getter(entity);
146+
}
147+
}
148+
}
149+
}

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Program.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ public static void Main(string[] args)
1313
.Create(DefaultConfig.Instance)
1414
.WithOptions(ConfigOptions.DisableOptimizationsValidator);
1515

16+
// Micro benchmark for value getters
17+
BenchmarkRunner.Run<GetValueComparator>(config);
18+
19+
// Library comparison benchmarks
1620
var comparators = new[]
1721
{
1822
typeof(LibComparatorMySql),

0 commit comments

Comments
 (0)