Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified images/bench-mysql.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/bench-postgresql.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/bench-sqlite.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/bench-sqlserver.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

using PhenX.EntityFrameworkCore.BulkInsert.Abstractions;
using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
using PhenX.EntityFrameworkCore.BulkInsert.Options;

namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata;

internal sealed class ColumnMetadata(IProperty property, SqlDialectBuilder dialect)
{
private readonly PropertyAccessor.Getter<object, object?> _getter = BuildGetter(property);
private readonly Func<object, object?> _getter = BuildGetter(property);

public IProperty Property { get; } = property;

Expand Down Expand Up @@ -44,28 +43,15 @@ internal sealed class ColumnMetadata(IProperty property, SqlDialectBuilder dial
return result;
}

private static PropertyAccessor.Getter<object, object?> BuildGetter(IProperty property)
private static Func<object, object?> BuildGetter(IProperty property)
{
var valueConverter =
property.GetValueConverter() ??
property.GetTypeMapping().Converter;

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)
Expand Down
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
Comment thread
PhenX marked this conversation as resolved.
{
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
Expand Up @@ -6,6 +6,7 @@
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.Benchmark" />
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.Tests" />
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.MySql" />
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.PostgreSql" />
Expand Down
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

View workflow job for this annotation

GitHub Actions / build (net9.0, 9.0.x)

Unboxing a possibly null value.

Check warning on line 33 in tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/GetValueComparator.cs

View workflow job for this annotation

GitHub Actions / build (net9.0, 9.0.x)

Unboxing a possibly null value.

Check warning on line 33 in tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/GetValueComparator.cs

View workflow job for this annotation

GitHub Actions / build (net8.0, 8.0.x)

Unboxing a possibly null value.

Check warning on line 33 in tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/GetValueComparator.cs

View workflow job for this annotation

GitHub Actions / build (net8.0, 8.0.x)

Unboxing a possibly null value.
};

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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public static void Main(string[] args)
.Create(DefaultConfig.Instance)
.WithOptions(ConfigOptions.DisableOptimizationsValidator);

// Micro benchmark for value getters
BenchmarkRunner.Run<GetValueComparator>(config);

// Library comparison benchmarks
var comparators = new[]
{
typeof(LibComparatorMySql),
Expand Down