-
-
Notifications
You must be signed in to change notification settings - Fork 10
Feature/improve allocations #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,66 +1,53 @@ | ||
| using System.Reflection.Emit; | ||
| using System.Linq.Expressions; | ||
| using System.Reflection; | ||
|
|
||
| namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata; | ||
|
|
||
| internal static class PropertyAccessor | ||
| { | ||
| public delegate TValue Getter<TSource, TValue>(TSource source); | ||
|
|
||
| public static Getter<object, object?> CreateUntypedGetter(PropertyInfo propertyInfo, Type sourceType, Type valueType) | ||
| public static Func<object, object?> CreateGetter(PropertyInfo propertyInfo, LambdaExpression? converter = null) | ||
| { | ||
| var method = | ||
| typeof(PropertyAccessor).GetMethod(nameof(CreateInternalUntypedGetter), BindingFlags.NonPublic | BindingFlags.Static)! | ||
| .MakeGenericMethod(sourceType, valueType); | ||
|
|
||
| return (Getter<object, object?>)method.Invoke(null, [propertyInfo])!; | ||
| } | ||
| ArgumentNullException.ThrowIfNull(propertyInfo); | ||
| var getMethod = propertyInfo.GetMethod ?? throw new ArgumentException("Property does not have a getter."); | ||
|
|
||
| private static Getter<object, object?> CreateInternalUntypedGetter<TSource, TValue>(PropertyInfo propertyInfo) | ||
| { | ||
| var getter = CreateGetter<TSource, TValue>(propertyInfo); | ||
| var instanceParam = Expression.Parameter(typeof(object), "instance"); | ||
|
|
||
| return source => getter((TSource)source!); | ||
| } | ||
| // Convert object to the declaring type | ||
| Expression typedInstance = propertyInfo.DeclaringType!.IsValueType | ||
| ? Expression.Unbox(instanceParam, propertyInfo.DeclaringType) | ||
| : Expression.Convert(instanceParam, propertyInfo.DeclaringType); | ||
|
|
||
| public static Getter<TSource, TValue> CreateGetter<TSource, TValue>(PropertyInfo propertyInfo) | ||
| { | ||
| if (!propertyInfo.CanRead) | ||
| { | ||
| return x => throw new NotSupportedException(); | ||
| } | ||
| // Call Getter | ||
| Expression getterExpression = Expression.Call(typedInstance, getMethod); | ||
|
|
||
| var bakingField = | ||
| propertyInfo.DeclaringType!.GetField($"<{propertyInfo.Name}>k__BackingField", | ||
| BindingFlags.NonPublic | | ||
| BindingFlags.Instance); | ||
| var propertyType = propertyInfo.PropertyType; | ||
|
|
||
| var propertyGetMethod = propertyInfo.GetGetMethod()!; | ||
|
|
||
| var getMethod = new DynamicMethod(propertyGetMethod.Name, typeof(TValue), [typeof(TSource)], true); | ||
| var getGenerator = getMethod.GetILGenerator(); | ||
|
|
||
| // Load this to stack. | ||
| getGenerator.Emit(OpCodes.Ldarg_0); | ||
|
|
||
| if (bakingField != null && !propertyGetMethod.IsVirtual) | ||
| { | ||
| // Get field directly. | ||
| getGenerator.Emit(OpCodes.Ldfld, bakingField); | ||
| } | ||
| else if (propertyGetMethod.IsVirtual) | ||
| { | ||
| // Call the virtual property. | ||
| getGenerator.Emit(OpCodes.Callvirt, propertyGetMethod); | ||
| } | ||
| else | ||
| // If the converter is provided, we call it | ||
| if (converter != null) | ||
| { | ||
| // Call the non virtual property. | ||
| getGenerator.Emit(OpCodes.Call, propertyGetMethod); | ||
| // Validate the converter input type matches property type | ||
| var converterParamType = converter.Parameters[0].Type; | ||
| if (!converterParamType.IsAssignableFrom(propertyType) && !propertyType.IsAssignableFrom(converterParamType)) | ||
| { | ||
| throw new ArgumentException($"Converter input must be assignable from property type ({propertyType} -> {converterParamType})"); | ||
| } | ||
|
|
||
| // If property type != converter param, convert | ||
| var converterInput = getterExpression; | ||
| if (converterParamType != propertyType) | ||
| { | ||
| converterInput = Expression.Convert(getterExpression, converterParamType); | ||
| } | ||
|
|
||
| getterExpression = Expression.Invoke(converter, converterInput); | ||
|
|
||
| propertyType = getterExpression.Type; | ||
| } | ||
|
|
||
| getGenerator.Emit(OpCodes.Ret); | ||
| var finalExpression = propertyType.IsValueType | ||
| ? Expression.Convert(getterExpression, typeof(object)) | ||
| : getterExpression; | ||
|
|
||
| return getMethod.CreateDelegate<Getter<TSource, TValue>>(); | ||
| return Expression.Lambda<Func<object, object?>>(finalExpression, instanceParam).Compile(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| using System.Reflection; | ||
| using System.Reflection.Emit; | ||
|
|
||
| namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark; | ||
|
|
||
| public partial class GetValueComparator | ||
| { | ||
| public static Func<object, object?> CreateUntypedGetter(PropertyInfo propertyInfo, Type sourceType, Type valueType) | ||
| { | ||
| var method = | ||
| typeof(GetValueComparator).GetMethod(nameof(CreateInternalUntypedGetter), BindingFlags.NonPublic | BindingFlags.Static)! | ||
| .MakeGenericMethod(sourceType, valueType); | ||
|
|
||
| return (Func<object, object?>)method.Invoke(null, [propertyInfo])!; | ||
| } | ||
|
|
||
| private static Func<object, object?> CreateInternalUntypedGetter<TSource, TValue>(PropertyInfo propertyInfo) | ||
| { | ||
| var getter = CreateGetter<TSource, TValue>(propertyInfo); | ||
|
|
||
| return source => getter((TSource)source!); | ||
| } | ||
|
|
||
| public static Func<TSource, TValue> CreateGetter<TSource, TValue>(PropertyInfo propertyInfo) | ||
| { | ||
| if (!propertyInfo.CanRead) | ||
| { | ||
| return x => throw new NotSupportedException(); | ||
| } | ||
|
|
||
| var bakingField = | ||
| propertyInfo.DeclaringType!.GetField($"<{propertyInfo.Name}>k__BackingField", | ||
| BindingFlags.NonPublic | | ||
| BindingFlags.Instance); | ||
|
|
||
| var propertyGetMethod = propertyInfo.GetGetMethod()!; | ||
|
|
||
| var getMethod = new DynamicMethod(propertyGetMethod.Name, typeof(TValue), [typeof(TSource)], true); | ||
| var getGenerator = getMethod.GetILGenerator(); | ||
|
|
||
| // Load this to stack. | ||
| getGenerator.Emit(OpCodes.Ldarg_0); | ||
|
|
||
| if (bakingField != null && !propertyGetMethod.IsVirtual) | ||
| { | ||
| // Get field directly. | ||
| getGenerator.Emit(OpCodes.Ldfld, bakingField); | ||
| } | ||
| else if (propertyGetMethod.IsVirtual) | ||
| { | ||
| // Call the virtual property. | ||
| getGenerator.Emit(OpCodes.Callvirt, propertyGetMethod); | ||
| } | ||
| else | ||
| { | ||
| // Call the non virtual property. | ||
| getGenerator.Emit(OpCodes.Call, propertyGetMethod); | ||
| } | ||
|
|
||
| getGenerator.Emit(OpCodes.Ret); | ||
|
|
||
| return getMethod.CreateDelegate<Func<TSource, TValue>>(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| using System.Linq.Expressions; | ||
| using System.Reflection; | ||
|
|
||
| using BenchmarkDotNet.Attributes; | ||
| using BenchmarkDotNet.Engines; | ||
|
|
||
| using PhenX.EntityFrameworkCore.BulkInsert.Metadata; | ||
|
|
||
| namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark; | ||
|
|
||
| [MemoryDiagnoser] | ||
| [SimpleJob(RunStrategy.ColdStart, launchCount: 1, warmupCount: 0, iterationCount: 10)] | ||
| public partial class GetValueComparator | ||
| { | ||
| [Params(1_000_000)] public int N; | ||
|
|
||
| private IReadOnlyList<TestEntity> data = []; | ||
|
|
||
| [IterationSetup] | ||
| public void IterationSetup() | ||
| { | ||
| data = Enumerable.Range(1, N).Select(i => new TestEntity | ||
| { | ||
| Name = $"Entity{i}", | ||
| Price = (decimal)(i * 0.1), | ||
| Identifier = Guid.NewGuid(), | ||
| NumericEnumValue = (NumericEnum)(i % 2), | ||
| }).ToList(); | ||
| } | ||
|
|
||
| private static Dictionary<string, Expression<Func<object?, object?>>> Converters = new() | ||
| { | ||
| { nameof(TestEntity.NumericEnumValue), v => (int) v}, | ||
|
Check warning on line 33 in tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/GetValueComparator.cs
|
||
| }; | ||
|
|
||
| private static readonly PropertyInfo[] PropertyInfos = typeof(TestEntity).GetProperties(); | ||
|
|
||
| private static readonly Func<object, object?>[] PropertyInfoGetValueGetters = PropertyInfos | ||
| .Select<PropertyInfo, Func<object, object?>>(propertyInfo => | ||
| { | ||
| var converter = Converters.TryGetValue(propertyInfo.Name, out var expression) | ||
| ? expression.Compile() | ||
| : null; | ||
|
|
||
| if (converter == null) | ||
| { | ||
| return propertyInfo.GetValue; | ||
| } | ||
|
|
||
| return entity => converter(propertyInfo.GetValue(entity)); | ||
| }) | ||
| .ToArray(); | ||
|
|
||
| private static readonly Func<object, object?>[] PropertyInfoIlGetters = PropertyInfos | ||
| .Select<PropertyInfo, Func<object, object?>>(propertyInfo => | ||
| { | ||
| var converter = Converters.TryGetValue(propertyInfo.Name, out var expression) | ||
| ? expression.Compile() | ||
| : null; | ||
|
|
||
| var getter = CreateUntypedGetter(propertyInfo, propertyInfo.DeclaringType!, propertyInfo.PropertyType); | ||
|
|
||
| if (converter == null) | ||
| { | ||
| return getter; | ||
| } | ||
|
|
||
| return entity => converter(getter(entity)); | ||
| }) | ||
| .ToArray(); | ||
|
|
||
| private static readonly Func<object, object?>[] PropertyAccessorGetters = PropertyInfos | ||
| .Select<PropertyInfo, Func<object, object?>>(propertyInfo => | ||
| { | ||
| var converter = Converters.TryGetValue(propertyInfo.Name, out var expression) | ||
| ? expression | ||
| : null; | ||
|
|
||
| return PropertyAccessor.CreateGetter(propertyInfo, converter); | ||
| }) | ||
| .ToArray(); | ||
|
|
||
| [Benchmark(Baseline = true)] | ||
| public void Native() | ||
| { | ||
| var enumConverter = Converters[nameof(TestEntity.NumericEnumValue)].Compile(); | ||
|
|
||
| for (var i = 0; i < data.Count; i++) | ||
| { | ||
| var entity = data[i]; | ||
|
|
||
| _ = entity.Id; | ||
| _ = entity.Name; | ||
| _ = entity.Price; | ||
| _ = entity.Identifier; | ||
| _ = enumConverter(entity.NumericEnumValue); | ||
| _ = entity.CreatedAt; | ||
| _ = entity.UpdatedAt; | ||
| } | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public void PropertyInfo_GetValue() | ||
| { | ||
| for (var i = 0; i < data.Count; i++) | ||
| { | ||
| var entity = data[i]; | ||
|
|
||
| for (var j = 0; j < PropertyInfoGetValueGetters.Length; j++) | ||
| { | ||
| var getter = PropertyInfoGetValueGetters[j]; | ||
|
|
||
| _ = getter(entity); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public void ExpressionTreeGetter() | ||
| { | ||
| for (var i = 0; i < data.Count; i++) | ||
| { | ||
| var entity = data[i]; | ||
|
|
||
| for (var j = 0; j < PropertyAccessorGetters.Length; j++) | ||
| { | ||
| var getter = PropertyAccessorGetters[j]; | ||
|
|
||
| _ = getter(entity); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public void IlGetter() | ||
| { | ||
| for (var i = 0; i < data.Count; i++) | ||
| { | ||
| var entity = data[i]; | ||
|
|
||
| for (var j = 0; j < PropertyInfoIlGetters.Length; j++) | ||
| { | ||
| var getter = PropertyInfoIlGetters[j]; | ||
|
|
||
| _ = getter(entity); | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.