From 04efeb0668a5eaaf7ae1966f711f9a0da6eb8c10 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 29 Aug 2018 20:09:45 +0200 Subject: [PATCH] Use cached delegates to set property values rather than SetValue (#170) * Use cached delegates to set property values rather than SetValue * Minor refactoring. Added previous code comments. --- Source/Benchmark/BenchGenerate.cs | 74 +++++++++++++++++++ Source/Benchmark/Program.cs | 16 ++-- .../Extensions/ExtensionsForPropertyInfo.cs | 36 +++++++++ Source/Bogus/Faker[T].cs | 61 ++++++++++----- 4 files changed, 160 insertions(+), 27 deletions(-) create mode 100644 Source/Benchmark/BenchGenerate.cs create mode 100644 Source/Bogus/Extensions/ExtensionsForPropertyInfo.cs diff --git a/Source/Benchmark/BenchGenerate.cs b/Source/Benchmark/BenchGenerate.cs new file mode 100644 index 00000000..c67b938f --- /dev/null +++ b/Source/Benchmark/BenchGenerate.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes.Exporters; +using Bogus; + +namespace Benchmark +{ + [MarkdownExporter, MemoryDiagnoser] + public class BenchGenerate + { + public class Project + { + public long Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + + private static Faker FakerDefault { get; set; } + private static Faker FakerCustom { get; set; } + private static Faker FakerWithRules { get; set; } + private static Faker FakerWithRulesComplex { get; set; } + + [GlobalSetup] + public void Setup() + { + FakerDefault = new Faker().UseSeed(1337); + FakerCustom = new Faker() + .CustomInstantiator(f=> new Project()) + .UseSeed(1337); + FakerWithRules = new Faker() + .CustomInstantiator(f=> new Project()) + .RuleFor(p=>p.Id, f => f.IndexGlobal) + .UseSeed(1337); + FakerWithRulesComplex = new Faker() + .CustomInstantiator(f=> new Project()) + .RuleFor(p=>p.Id, f => f.IndexGlobal) + .RuleFor(p => p.Name, f => f.Person.Company.Name + f.UniqueIndex.ToString()) + .RuleFor(p => p.Description, f => f.Lorem.Paragraphs(3)) + .UseSeed(1337); + } + +// [Benchmark] + public void Generate_Default() + { + var projects = FakerDefault.Generate(10_000).ToList(); + } + +// [Benchmark] + public void Generate_CustomInstantiator() + { + var projects = FakerDefault.Generate(10_000).ToList(); + } + + [Benchmark] + public void Generate_WithRules() + { + var projects = FakerWithRules.Generate(10_000).ToList(); + } + + //[Benchmark] + public void Generate_WithRulesComplex() + { + var projects = FakerWithRulesComplex.Generate(10_000).ToList(); + } + +// [Benchmark] + public void Constructor() + { + var projects = Enumerable.Range(0, 10_000).Select(i => new Project {Id = i}).ToList(); + } + } +} \ No newline at end of file diff --git a/Source/Benchmark/Program.cs b/Source/Benchmark/Program.cs index efc791e4..68bea445 100644 --- a/Source/Benchmark/Program.cs +++ b/Source/Benchmark/Program.cs @@ -2,11 +2,11 @@ namespace Benchmark { - class Program - { - static void Main() - { - BenchmarkRunner.Run(); - } - } -} + class Program + { + static void Main() + { + BenchmarkRunner.Run(); + } + } +} \ No newline at end of file diff --git a/Source/Bogus/Extensions/ExtensionsForPropertyInfo.cs b/Source/Bogus/Extensions/ExtensionsForPropertyInfo.cs new file mode 100644 index 00000000..b5e164a4 --- /dev/null +++ b/Source/Bogus/Extensions/ExtensionsForPropertyInfo.cs @@ -0,0 +1,36 @@ +using System; +using System.Reflection; + +namespace Bogus.Extensions +{ + public static class ExtensionsForPropertyInfo + { + private static readonly MethodInfo GenericSetterCreationMethod = + typeof(ExtensionsForPropertyInfo).GetMethod(nameof(CreateSetterGeneric), BindingFlags.Static | BindingFlags.NonPublic); + + public static Action CreateSetter(this PropertyInfo property) + { + if (property == null) throw new ArgumentNullException(nameof(property)); + + var setter = property.GetSetMethod(); + if (setter == null) throw new ArgumentException("The specified property does not have a public setter."); + + var genericHelper = GenericSetterCreationMethod.MakeGenericMethod(property.DeclaringType, property.PropertyType); + return (Action)genericHelper.Invoke(null, new object[] { setter }); + } + + private static Action CreateSetterGeneric(MethodInfo setter) where T : class + { + var setterTypedDelegate = +#if STANDARD + (Action) setter.CreateDelegate(typeof(Action)) +#else + (Action) Delegate.CreateDelegate(typeof(Action), setter) +#endif + ; + var setterDelegate = (Action)((T instance, object value) => { setterTypedDelegate(instance, (V)value); }); + return setterDelegate; + } + + } +} \ No newline at end of file diff --git a/Source/Bogus/Faker[T].cs b/Source/Bogus/Faker[T].cs index 3ba1bd52..0d1410a5 100644 --- a/Source/Bogus/Faker[T].cs +++ b/Source/Bogus/Faker[T].cs @@ -4,6 +4,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Text; +using Bogus.Extensions; namespace Bogus { @@ -53,6 +54,8 @@ public class Faker : IFakerTInternal, ILocaleAware, IRuleSet where T : cla protected internal readonly Dictionary> FinalizeActions = new Dictionary>(StringComparer.OrdinalIgnoreCase); protected internal Dictionary> CreateActions = new Dictionary>(StringComparer.OrdinalIgnoreCase); protected internal readonly Dictionary TypeProperties; + protected internal readonly Dictionary> SetterCache = new Dictionary>(StringComparer.OrdinalIgnoreCase); + protected internal Dictionary StrictModes = new Dictionary(); protected internal bool? IsValid; protected internal string currentRuleSet = Default; @@ -526,8 +529,6 @@ protected virtual void PopulateInternal(T instance, string[] ruleSets) throw MakeValidationException(vr ?? ValidateInternal(ruleSets)); } - var typeProps = TypeProperties; - lock( Randomizer.Locker.Value ) { //Issue 57 - Make sure you generate a new context @@ -543,23 +544,7 @@ protected virtual void PopulateInternal(T instance, string[] ruleSets) { foreach( var action in populateActions.Values ) { - typeProps.TryGetValue(action.PropertyName, out MemberInfo member); - var valueFactory = action.Action; - if( valueFactory is null ) continue; // An .Ignore() rule. - - if( member != null ) - { - var prop = member as PropertyInfo; - prop?.SetValue(instance, valueFactory(FakerHub, instance), null); - - var field = member as FieldInfo; - field?.SetValue(instance, valueFactory(FakerHub, instance)); - } - else // member would be null if this was an RuleForObject. - { - //Invoke this if this is a basic rule which does not select a property or a field. - var outputValue = valueFactory(FakerHub, instance); - } + PopulateProperty(instance, action); } } } @@ -574,6 +559,44 @@ protected virtual void PopulateInternal(T instance, string[] ruleSets) } } + private readonly object _setterCreateLock = new object(); + private void PopulateProperty(T instance, PopulateAction action) + { + var valueFactory = action.Action; + if (valueFactory is null) return; // An .Ignore() rule. + + var value = valueFactory(FakerHub, instance); + + if (SetterCache.TryGetValue(action.PropertyName, out var setter)) + { + setter(instance, value); + return; + } + + if (!TypeProperties.TryGetValue(action.PropertyName, out var member)) return; + if (member == null) return; // Member would be null if this was a .Rules() + // The valueFactory is already invoked + // which does not select a property or field. + + lock (_setterCreateLock) + { + if (SetterCache.TryGetValue(action.PropertyName, out setter)) + { + setter(instance, value); + return; + } + + if (member is PropertyInfo prop) + setter = prop.CreateSetter(); + // TODO FieldInfo will need to rely on ILEmit to create a delegate + else if (member is FieldInfo field) + setter = (i, v) => field?.SetValue(i, v); + if (setter == null) return; + + SetterCache.Add(action.PropertyName, setter); + setter(instance, value); + } + } /// /// When is enabled, checks if all properties or fields of have /// rules defined. Returns true if all rules are defined, false otherwise.