diff --git a/src/NHibernate.Test/PropertyTest/AccessorPerformanceFixture.cs b/src/NHibernate.Test/PropertyTest/AccessorPerformanceFixture.cs new file mode 100644 index 00000000000..9cc77aa43f3 --- /dev/null +++ b/src/NHibernate.Test/PropertyTest/AccessorPerformanceFixture.cs @@ -0,0 +1,108 @@ +using System; +using System.Diagnostics; +using NHibernate.Properties; +using NUnit.Framework; + +namespace NHibernate.Test.PropertyTest +{ + public abstract class AccessorPerformanceFixture where T : new() + { + private IPropertyAccessor _accessor; + private ISetter _reflectionSetter; + private ISetter _ilSetter; + private IGetter _reflectionGetter; + private IGetter _ilGetter; + + protected abstract string AccessorType { get; } + + protected abstract string Path { get; } + + protected abstract object GetValue(); + + [SetUp] + public void SetUp() + { + _accessor = PropertyAccessorFactory.GetPropertyAccessor(AccessorType); + var optimize = Cfg.Environment.UseReflectionOptimizer; + + try + { + Cfg.Environment.UseReflectionOptimizer = false; + _reflectionSetter = _accessor.GetSetter(typeof(T), Path); + _reflectionGetter = _accessor.GetGetter(typeof(T), Path); + + Cfg.Environment.UseReflectionOptimizer = true; + _ilSetter = _accessor.GetSetter(typeof(T), Path); + _ilGetter = _accessor.GetGetter(typeof(T), Path); + } + finally + { + Cfg.Environment.UseReflectionOptimizer = optimize; + } + } + + [TestCase(50000)] + [TestCase(100000)] + [TestCase(200000)] + [TestCase(500000)] + public void TestGet(int iter) + { + var target = new T(); + var stopwatch = new Stopwatch(); + + // Warm up + TestGetter(_reflectionGetter, target, 100); + TestGetter(_ilGetter, target, 100); + + stopwatch.Restart(); + TestGetter(_reflectionGetter, target, iter); + stopwatch.Stop(); + Console.WriteLine($"Reflection getter total time for {iter} iterations: {stopwatch.ElapsedMilliseconds}ms"); + + stopwatch.Restart(); + TestGetter(_ilGetter, target, iter); + stopwatch.Stop(); + Console.WriteLine($"IL getter total time for {iter} iterations: {stopwatch.ElapsedMilliseconds}ms"); + } + + [TestCase(50000)] + [TestCase(100000)] + [TestCase(200000)] + [TestCase(500000)] + public void TestSet(int iter) + { + var target = new T(); + var stopwatch = new Stopwatch(); + + // Warm up + TestSetter(_reflectionSetter, target, 100); + TestSetter(_ilSetter, target, 100); + + stopwatch.Restart(); + TestSetter(_reflectionSetter, target, iter); + stopwatch.Stop(); + Console.WriteLine($"Reflection setter total time for {iter} iterations: {stopwatch.ElapsedMilliseconds}ms"); + + stopwatch.Restart(); + TestSetter(_ilSetter, target, iter); + stopwatch.Stop(); + Console.WriteLine($"IL setter total time for {iter} iterations: {stopwatch.ElapsedMilliseconds}ms"); + } + + private static void TestGetter(IGetter getter, object target, int iter) + { + for (var i = 0; i < iter; i++) + { + var val = getter.Get(target); + } + } + + private void TestSetter(ISetter setter, object target, int iter) + { + for (var i = 0; i < iter; i++) + { + setter.Set(target, GetValue()); + } + } + } +} diff --git a/src/NHibernate.Test/PropertyTest/BasicPropertyAccessorPerformanceFixture.cs b/src/NHibernate.Test/PropertyTest/BasicPropertyAccessorPerformanceFixture.cs new file mode 100644 index 00000000000..f038b20ef4b --- /dev/null +++ b/src/NHibernate.Test/PropertyTest/BasicPropertyAccessorPerformanceFixture.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; + +namespace NHibernate.Test.PropertyTest +{ + [TestFixture, Explicit] + public class BasicPropertyAccessorPerformanceFixture : AccessorPerformanceFixture + { + protected override string AccessorType => "property"; + + protected override string Path => "Id"; + + protected override object GetValue() + { + return 5; + } + + public class A + { + public int Id { get; set; } + } + } +} diff --git a/src/NHibernate.Test/PropertyTest/FieldAccessorPerformanceFixture.cs b/src/NHibernate.Test/PropertyTest/FieldAccessorPerformanceFixture.cs new file mode 100644 index 00000000000..6f06f213a9d --- /dev/null +++ b/src/NHibernate.Test/PropertyTest/FieldAccessorPerformanceFixture.cs @@ -0,0 +1,24 @@ +using NUnit.Framework; + +namespace NHibernate.Test.PropertyTest +{ + [TestFixture, Explicit] + public class FieldAccessorPerformanceFixture : AccessorPerformanceFixture + { + protected override string AccessorType => "field"; + + protected override string Path => "_id"; + + protected override object GetValue() + { + return 10; + } + + public class A + { + private int _id = 5; + + public int Id => _id; + } + } +} diff --git a/src/NHibernate/Properties/BasicPropertyAccessor.cs b/src/NHibernate/Properties/BasicPropertyAccessor.cs index 18399171f31..a812fa1febd 100644 --- a/src/NHibernate/Properties/BasicPropertyAccessor.cs +++ b/src/NHibernate/Properties/BasicPropertyAccessor.cs @@ -1,7 +1,9 @@ using System; using System.Collections; +using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; +using System.Runtime.Serialization; using NHibernate.Engine; namespace NHibernate.Properties @@ -159,11 +161,13 @@ internal static BasicSetter GetSetterOrNull(System.Type type, string propertyNam /// An for a Property get. /// [Serializable] - public sealed class BasicGetter : IGetter, IOptimizableGetter + public sealed class BasicGetter : IGetter, IOptimizableGetter, IDeserializationCallback { private readonly System.Type clazz; private readonly PropertyInfo property; private readonly string propertyName; + [NonSerialized] + private Lazy> _getDelegate; /// /// Initializes a new instance of . @@ -176,6 +180,8 @@ public BasicGetter(System.Type clazz, PropertyInfo property, string propertyName this.clazz = clazz; this.property = property; this.propertyName = propertyName; + + SetupDelegate(); } public PropertyInfo Property @@ -196,7 +202,9 @@ public object Get(object target) { try { - return property.GetValue(target, Array.Empty()); + return _getDelegate == null + ? property.GetValue(target, Array.Empty()) + : _getDelegate.Value(target); } catch (Exception e) { @@ -249,17 +257,45 @@ public void Emit(ILGenerator il) } il.EmitCall(OpCodes.Callvirt, method, null); } + + public void OnDeserialization(object sender) + { + SetupDelegate(); + } + + private void SetupDelegate() + { + if (!Cfg.Environment.UseReflectionOptimizer) + { + return; + } + + _getDelegate = new Lazy>(CreateDelegate); + } + + private Func CreateDelegate() + { + var targetParameter = Expression.Parameter(typeof(object), "t"); + return Expression.Lambda>( + Expression.Convert( + Expression.Call(Expression.Convert(targetParameter, clazz), Method), + typeof(object)), + targetParameter) + .Compile(); + } } /// /// An for a Property set. /// [Serializable] - public sealed class BasicSetter : ISetter, IOptimizableSetter + public sealed class BasicSetter : ISetter, IOptimizableSetter, IDeserializationCallback { private readonly System.Type clazz; private readonly PropertyInfo property; private readonly string propertyName; + [NonSerialized] + private Lazy> _setDelegate; /// /// Initializes a new instance of . @@ -272,6 +308,8 @@ public BasicSetter(System.Type clazz, PropertyInfo property, string propertyName this.clazz = clazz; this.property = property; this.propertyName = propertyName; + + SetupDelegate(); } public PropertyInfo Property @@ -293,27 +331,23 @@ public void Set(object target, object value) { try { - property.SetValue(target, value, Array.Empty()); - } - catch (ArgumentException ae) - { - // if I'm reading the msdn docs correctly this is the only reason the ArgumentException - // would be thrown, but it doesn't hurt to make sure. - if (property.PropertyType.IsInstanceOfType(value) == false) + if (_setDelegate == null) { - // add some details to the error message - there have been a few forum posts an they are - // all related to an ISet and IDictionary mixups. - string msg = - String.Format("The type {0} can not be assigned to a property of type {1}", value.GetType(), - property.PropertyType); - throw new PropertyAccessException(ae, msg, true, clazz, propertyName); + property.SetValue(target, value, Array.Empty()); } else { - throw new PropertyAccessException(ae, "ArgumentException while setting the property value by reflection", true, - clazz, propertyName); + _setDelegate.Value(target, value); } } + catch (InvalidCastException ce) + { + HandleException(ce, value); + } + catch (ArgumentException ae) + { + HandleException(ae, value); + } catch (Exception e) { throw new PropertyAccessException(e, "could not set a property value by reflection", true, clazz, propertyName); @@ -354,6 +388,58 @@ public void Emit(ILGenerator il) } il.EmitCall(OpCodes.Callvirt, method, null); } + + public void OnDeserialization(object sender) + { + SetupDelegate(); + } + + private void SetupDelegate() + { + if (!Cfg.Environment.UseReflectionOptimizer) + { + return; + } + + _setDelegate = new Lazy>(CreateDelegate); + } + + private void HandleException(Exception e, object value) + { + // if I'm reading the msdn docs correctly this is the only reason the ArgumentException + // would be thrown, but it doesn't hurt to make sure. + if (property.PropertyType.IsInstanceOfType(value) == false) + { + // add some details to the error message - there have been a few forum posts an they are + // all related to an ISet and IDictionary mixups. + string msg = + String.Format("The type {0} can not be assigned to a property of type {1}", value.GetType(), + property.PropertyType); + throw new PropertyAccessException(e, msg, true, clazz, propertyName); + } + + if (_setDelegate == null) + { + throw new PropertyAccessException(e, "ArgumentException while setting the property value by reflection", true, + clazz, propertyName); + } + + throw new PropertyAccessException(e, "could not set a property value by reflection", true, clazz, propertyName); + } + + private Action CreateDelegate() + { + var targetParameter = Expression.Parameter(typeof(object), "t"); + var valueParameter = Expression.Parameter(typeof(object), "p"); + return Expression.Lambda>( + Expression.Call( + Expression.Convert(targetParameter, clazz), + Method, + Expression.Convert(valueParameter, property.PropertyType)), + targetParameter, + valueParameter) + .Compile(); + } } } } diff --git a/src/NHibernate/Properties/FieldAccessor.cs b/src/NHibernate/Properties/FieldAccessor.cs index 5a4b4df3854..4a4227827d9 100644 --- a/src/NHibernate/Properties/FieldAccessor.cs +++ b/src/NHibernate/Properties/FieldAccessor.cs @@ -1,7 +1,9 @@ using System; using System.Collections; +using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; +using System.Runtime.Serialization; using NHibernate.Engine; using NHibernate.Util; @@ -158,11 +160,13 @@ private string GetFieldName(string propertyName) /// An that uses a Field instead of the Property get. /// [Serializable] - public sealed class FieldGetter : IGetter, IOptimizableGetter + public sealed class FieldGetter : IGetter, IOptimizableGetter, IDeserializationCallback { private readonly FieldInfo field; private readonly System.Type clazz; private readonly string name; + [NonSerialized] + private Lazy> _getDelegate; /// /// Initializes a new instance of . @@ -175,6 +179,8 @@ public FieldGetter(FieldInfo field, System.Type clazz, string name) this.field = field; this.clazz = clazz; this.name = name; + + SetupDelegate(); } #region IGetter Members @@ -190,7 +196,9 @@ public object Get(object target) { try { - return field.GetValue(target); + return _getDelegate == null + ? field.GetValue(target) + : _getDelegate.Value(target); } catch (Exception e) { @@ -236,17 +244,45 @@ public void Emit(ILGenerator il) { il.Emit(OpCodes.Ldfld, field); } + + public void OnDeserialization(object sender) + { + SetupDelegate(); + } + + private void SetupDelegate() + { + if (!Cfg.Environment.UseReflectionOptimizer) + { + return; + } + + _getDelegate = new Lazy>(CreateDelegate); + } + + private Func CreateDelegate() + { + var targetParameter = Expression.Parameter(typeof(object), "t"); + return Expression.Lambda>( + Expression.Convert( + Expression.Field(Expression.Convert(targetParameter, clazz), field), + typeof(object)), + targetParameter) + .Compile(); + } } /// /// An that uses a Field instead of the Property set. /// [Serializable] - public sealed class FieldSetter : ISetter, IOptimizableSetter + public sealed class FieldSetter : ISetter, IOptimizableSetter, IDeserializationCallback { private readonly FieldInfo field; private readonly System.Type clazz; private readonly string name; + [NonSerialized] + private Lazy> _setDelegate; /// /// Initializes a new instance of . @@ -259,6 +295,8 @@ public FieldSetter(FieldInfo field, System.Type clazz, string name) this.field = field; this.clazz = clazz; this.name = name; + + SetupDelegate(); } #region ISetter Members @@ -275,27 +313,23 @@ public void Set(object target, object value) { try { - field.SetValue(target, value); - } - catch (ArgumentException ae) - { - // if I'm reading the msdn docs correctly this is the only reason the ArgumentException - // would be thrown, but it doesn't hurt to make sure. - if (field.FieldType.IsInstanceOfType(value) == false) + if (_setDelegate == null) { - // add some details to the error message - there have been a few forum posts an they are - // all related to an ISet and IDictionary mixups. - string msg = - String.Format("The type {0} can not be assigned to a field of type {1}", value.GetType().ToString(), - field.FieldType.ToString()); - throw new PropertyAccessException(ae, msg, true, clazz, name); + field.SetValue(target, value); } else { - throw new PropertyAccessException(ae, "ArgumentException while setting the field value by reflection", true, clazz, - name); + _setDelegate.Value(target, value); } } + catch (InvalidCastException ce) + { + HandleException(ce, value); + } + catch (ArgumentException ae) + { + HandleException(ae, value); + } catch (Exception e) { throw new PropertyAccessException(e, "could not set a field value by reflection", true, clazz, name); @@ -331,6 +365,58 @@ public void Emit(ILGenerator il) { il.Emit(OpCodes.Stfld, field); } + + public void OnDeserialization(object sender) + { + SetupDelegate(); + } + + private void SetupDelegate() + { + // IL/Lambda are not able to set a readonly field + if (!Cfg.Environment.UseReflectionOptimizer || field.IsInitOnly) + { + return; + } + + _setDelegate = new Lazy>(CreateDelegate); + } + + private void HandleException(Exception e, object value) + { + // if I'm reading the msdn docs correctly this is the only reason the ArgumentException + // would be thrown, but it doesn't hurt to make sure. + if (field.FieldType.IsInstanceOfType(value) == false) + { + // add some details to the error message - there have been a few forum posts an they are + // all related to an ISet and IDictionary mixups. + string msg = + String.Format("The type {0} can not be assigned to a field of type {1}", value.GetType().ToString(), + field.FieldType.ToString()); + throw new PropertyAccessException(e, msg, true, clazz, name); + } + + if (_setDelegate == null) + { + throw new PropertyAccessException(e, "ArgumentException while setting the field value by reflection", true, clazz, + name); + } + + throw new PropertyAccessException(e, "could not set a field value by reflection", true, clazz, name); + } + + private Action CreateDelegate() + { + var targetParameter = Expression.Parameter(typeof(object), "t"); + var valueParameter = Expression.Parameter(typeof(object), "p"); + return Expression.Lambda>( + Expression.Assign( + Expression.Field(Expression.Convert(targetParameter, clazz), field), + Expression.Convert(valueParameter, Type)), + targetParameter, + valueParameter) + .Compile(); + } } } }