diff --git a/images/bench-mysql.png b/images/bench-mysql.png index b4a14e1..cb646b3 100644 Binary files a/images/bench-mysql.png and b/images/bench-mysql.png differ diff --git a/images/bench-postgresql.png b/images/bench-postgresql.png index da96f25..b6f1767 100644 Binary files a/images/bench-postgresql.png and b/images/bench-postgresql.png differ diff --git a/images/bench-sqlite.png b/images/bench-sqlite.png index 4a3c6e3..6845658 100644 Binary files a/images/bench-sqlite.png and b/images/bench-sqlite.png differ diff --git a/images/bench-sqlserver.png b/images/bench-sqlserver.png index bce6c63..6e067c2 100644 Binary files a/images/bench-sqlserver.png and b/images/bench-sqlserver.png differ diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs index 3d51957..3d1c899 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs @@ -1,7 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; -using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; using PhenX.EntityFrameworkCore.BulkInsert.Dialect; using PhenX.EntityFrameworkCore.BulkInsert.Options; @@ -9,7 +8,7 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata; internal sealed class ColumnMetadata(IProperty property, SqlDialectBuilder dialect) { - private readonly PropertyAccessor.Getter _getter = BuildGetter(property); + private readonly Func _getter = BuildGetter(property); public IProperty Property { get; } = property; @@ -44,7 +43,7 @@ internal sealed class ColumnMetadata(IProperty property, SqlDialectBuilder dial return result; } - private static PropertyAccessor.Getter BuildGetter(IProperty property) + private static Func BuildGetter(IProperty property) { var valueConverter = property.GetValueConverter() ?? @@ -52,20 +51,7 @@ internal sealed class ColumnMetadata(IProperty property, SqlDialectBuilder dial var propInfo = property.PropertyInfo!; - var actualGetter = - PropertyAccessor.CreateUntypedGetter( - propInfo, - property.DeclaringType.ClrType, - property.ClrType); - - if (valueConverter == null) - { - return actualGetter; - } - - var converter = valueConverter.ConvertToProvider; - - return source => converter(actualGetter(source)); + return PropertyAccessor.CreateGetter(propInfo, valueConverter?.ConvertToProviderExpression); } private static string GetStoreDefinition(IProperty property) diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/PropertyAccessor.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/PropertyAccessor.cs index b503899..8012739 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/PropertyAccessor.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/PropertyAccessor.cs @@ -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 source); - - public static Getter CreateUntypedGetter(PropertyInfo propertyInfo, Type sourceType, Type valueType) + public static Func CreateGetter(PropertyInfo propertyInfo, LambdaExpression? converter = null) { - var method = - typeof(PropertyAccessor).GetMethod(nameof(CreateInternalUntypedGetter), BindingFlags.NonPublic | BindingFlags.Static)! - .MakeGenericMethod(sourceType, valueType); - - return (Getter)method.Invoke(null, [propertyInfo])!; - } + ArgumentNullException.ThrowIfNull(propertyInfo); + var getMethod = propertyInfo.GetMethod ?? throw new ArgumentException("Property does not have a getter."); - private static Getter CreateInternalUntypedGetter(PropertyInfo propertyInfo) - { - var getter = CreateGetter(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 CreateGetter(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>(); + return Expression.Lambda>(finalExpression, instanceParam).Compile(); } } diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/PhenX.EntityFrameworkCore.BulkInsert.csproj b/src/PhenX.EntityFrameworkCore.BulkInsert/PhenX.EntityFrameworkCore.BulkInsert.csproj index 5ee0a22..90be5d2 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/PhenX.EntityFrameworkCore.BulkInsert.csproj +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/PhenX.EntityFrameworkCore.BulkInsert.csproj @@ -6,6 +6,7 @@ + diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/GetValueComparator.IlGetter.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/GetValueComparator.IlGetter.cs new file mode 100644 index 0000000..d63a0a7 --- /dev/null +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/GetValueComparator.IlGetter.cs @@ -0,0 +1,64 @@ +using System.Reflection; +using System.Reflection.Emit; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark; + +public partial class GetValueComparator +{ + public static Func CreateUntypedGetter(PropertyInfo propertyInfo, Type sourceType, Type valueType) + { + var method = + typeof(GetValueComparator).GetMethod(nameof(CreateInternalUntypedGetter), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(sourceType, valueType); + + return (Func)method.Invoke(null, [propertyInfo])!; + } + + private static Func CreateInternalUntypedGetter(PropertyInfo propertyInfo) + { + var getter = CreateGetter(propertyInfo); + + return source => getter((TSource)source!); + } + + public static Func CreateGetter(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>(); + } +} diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/GetValueComparator.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/GetValueComparator.cs new file mode 100644 index 0000000..696b409 --- /dev/null +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/GetValueComparator.cs @@ -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 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>> Converters = new() + { + { nameof(TestEntity.NumericEnumValue), v => (int) v}, + }; + + private static readonly PropertyInfo[] PropertyInfos = typeof(TestEntity).GetProperties(); + + private static readonly Func[] PropertyInfoGetValueGetters = PropertyInfos + .Select>(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[] PropertyInfoIlGetters = PropertyInfos + .Select>(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[] PropertyAccessorGetters = PropertyInfos + .Select>(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); + } + } + } +} diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Program.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Program.cs index 60b844f..67edc71 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Program.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Program.cs @@ -13,6 +13,10 @@ public static void Main(string[] args) .Create(DefaultConfig.Instance) .WithOptions(ConfigOptions.DisableOptimizationsValidator); + // Micro benchmark for value getters + BenchmarkRunner.Run(config); + + // Library comparison benchmarks var comparators = new[] { typeof(LibComparatorMySql),