2017-02-14 14 views
2

こんにちは、PropertyInfo.GetValue()/ SetValue()メソッド呼び出しを使用する場合よりもはるかに優れた(つまり高速な)結果が得られたことを確認するために、(キャッシュされた)コンパイル済みラムダ式をプロパティアクセスに使用しようとしていました。しかし、私はそれがまだ本当に "ネイティブ"プロパティの速度に近づいていると感じています。ベンチマーク手法は、他の人が得られる結果と大きく異なる結果になりますか?ここでコンパイルされたラムダ式がプロパティゲッタとセッタとして使用されています:間違ったベンチマーク方法または間違ったラムダ式の構築?

私は以下のコードの私の作品を実行した後に得た結果を下回っている:

Native: Elapsed = 00:00:00.0995876 (99.5876 ms); Step = 1.992E-005 ms 
Lambda Expression: Elapsed = 00:00:00.5369273 (536.9273 ms); Step = 1.074E-004 ms 
Property Info: Elapsed = 00:00:01.9187312 (1918.7312 ms); Step = 3.837E-004 ms 

1.000 < 5.392 < 19.267 

正直言って、私は他のベンチマークに基づいて、コンパイルされたラムダ式は通常のプロパティを使用するよりも2倍遅くなければならず、5〜6倍遅くならないと感じています。

ベンチマーク方法は?コンパイルされたラムダ式が計算される方法は?

public static class Program 
{ 
    public static void Main(params string[] args) 
    { 
     var stepCount = 5000000UL; 

     var dummy = new Dummy(); 

     const string propertyName = "Soother"; 

     const bool propertyValue = true; 

     var propertyInfo = typeof(Dummy).GetProperty(propertyName); 

     var nativeBenchmark = Benchmark.Run("Native", stepCount,() => dummy.Soother = propertyValue); 
     var lambdaExpressionBenchmark = Benchmark.Run("Lambda Expression", stepCount,() => dummy.Set(propertyName, propertyValue)); 
     var propertyInfoBenchmark = Benchmark.Run("Property Info", stepCount,() => propertyInfo.SetValue(dummy, propertyValue, null)); 

     var benchmarkReports = new[] { nativeBenchmark, lambdaExpressionBenchmark, propertyInfoBenchmark }.OrderBy(item => item.ElapsedMilliseconds); 

     benchmarkReports.Join(Environment.NewLine).WriteLineToConsole(); 

     var fastest = benchmarkReports.First().ElapsedMilliseconds; 

     benchmarkReports.Select(report => (report.ElapsedMilliseconds/fastest).ToString("0.000")).Join(" < ").WriteLineToConsole(); 

     Console.ReadKey(); 
    } 
} 

public class Dummy 
{ 
    public bool? Soother { get; set; } = true; 
} 

public class BenchMarkReport 
{ 
    #region Fields & Properties 

    public string Name { get; } 
    public TimeSpan ElapsedTime { get; } 
    public double ElapsedMilliseconds 
    { 
     get 
     { 
      return ElapsedTime.TotalMilliseconds; 
     } 
    } 
    public ulong StepCount { get; } 
    public double StepElapsedMilliseconds 
    { 
     get 
     { 
      return ElapsedMilliseconds/StepCount; 
     } 
    } 

    #endregion 

    #region Constructors 

    internal BenchMarkReport(string name, TimeSpan elapsedTime, ulong stepCount) 
    { 
     Name = name; 
     ElapsedTime = elapsedTime; 
     StepCount = stepCount; 
    } 

    #endregion 

    #region Methods 

    public override string ToString() 
    { 
     return $"{Name}: Elapsed = {ElapsedTime} ({ElapsedMilliseconds} ms); Step = {StepElapsedMilliseconds:0.###E+000} ms"; 
    } 

    #endregion 
} 

public class Benchmark 
{ 
    #region Fields & Properties 

    private readonly Action _stepAction; 

    public string Name { get; } 

    public ulong StepCount { get; } 

    public Benchmark(string name, ulong stepCount, Action stepAction) 
    { 
     Name = name; 
     StepCount = stepCount; 
     _stepAction = stepAction; 
    } 

    #endregion 

    #region Constructors 

    #endregion 

    #region Methods 

    public static BenchMarkReport Run(string name, ulong stepCount, Action stepAction) 
    { 
     var benchmark = new Benchmark(name, stepCount, stepAction); 

     var benchmarkReport = benchmark.Run(); 

     return benchmarkReport; 
    } 

    public BenchMarkReport Run() 
    { 
     return Run(StepCount); 
    } 

    public BenchMarkReport Run(ulong stepCountOverride) 
    { 
     var stopwatch = Stopwatch.StartNew(); 

     for (ulong i = 0; i < StepCount; i++) 
     { 
      _stepAction(); 
     } 

     stopwatch.Stop(); 

     var benchmarkReport = new BenchMarkReport(Name, stopwatch.Elapsed, stepCountOverride); 

     return benchmarkReport; 
    } 

    #endregion 
} 

public static class ObjectExtensions 
{ 
    public static void WriteToConsole<TInstance>(this TInstance instance) 
    { 
     Console.Write(instance); 
    } 

    public static void WriteLineToConsole<TInstance>(this TInstance instance) 
    { 
     Console.WriteLine(instance); 
    } 

    // Goodies: add name inference from property lambda expression 
    // e.g. "instance => instance.PropertyName" redirected using "PropertyName" 

    public static TProperty Get<TInstance, TProperty>(this TInstance instance, string propertyName) 
    { 
     return FastPropertyRepository<TInstance, TProperty>.GetGetter(propertyName)(instance); 
    } 

    public static void Set<TInstance, TProperty>(this TInstance instance, string propertyName, TProperty propertyValue) 
    { 
     FastPropertyRepository<TInstance, TProperty>.GetSetter(propertyName)(instance, propertyValue); 
    } 
} 

public static class EnumerableExtensions 
{ 
    public static string Join<TSource>(this IEnumerable<TSource> source, string separator = ", ") 
    { 
     return string.Join(separator, source); 
    } 
} 

internal static class FastPropertyRepository<TInstance, TProperty> 
{ 
    private static readonly IDictionary<string, Action<TInstance, TProperty>> Setters; 
    private static readonly IDictionary<string, Func<TInstance, TProperty>> Getters; 

    static FastPropertyRepository() 
    { 
     Getters = new ConcurrentDictionary<string, Func<TInstance, TProperty>>(); 
     Setters = new ConcurrentDictionary<string, Action<TInstance, TProperty>>(); 
    } 

    public static Func<TInstance, TProperty> GetGetter(string propertyName) 
    { 
     Func<TInstance, TProperty> getter; 
     if (!Getters.TryGetValue(propertyName, out getter)) 
     { 
      getter = FastPropertyFactory.GeneratePropertyGetter<TInstance, TProperty>(propertyName); 
      Getters[propertyName] = getter; 
     } 

     return getter; 
    } 

    public static Action<TInstance, TProperty> GetSetter(string propertyName) 
    { 
     Action<TInstance, TProperty> setter; 
     if (!Setters.TryGetValue(propertyName, out setter)) 
     { 
      setter = FastPropertyFactory.GeneratePropertySetter<TInstance, TProperty>(propertyName); 
      Setters[propertyName] = setter; 
     } 

     return setter; 
    } 
} 

internal static class FastPropertyFactory 
{ 
    public static Func<TInstance, TProperty> GeneratePropertyGetter<TInstance, TProperty>(string propertyName) 
    { 
     var parameterExpression = Expression.Parameter(typeof(TInstance), "value"); 

     var propertyValueExpression = Expression.Property(parameterExpression, propertyName); 

     var expression = propertyValueExpression.Type == typeof(TProperty) ? propertyValueExpression : (Expression)Expression.Convert(propertyValueExpression, typeof(TProperty)); 

     var propertyGetter = Expression.Lambda<Func<TInstance, TProperty>>(expression, parameterExpression).Compile(); 

     return propertyGetter; 
    } 

    public static Action<TInstance, TProperty> GeneratePropertySetter<TInstance, TProperty>(string propertyName) 
    { 
     var instanceParameterExpression = Expression.Parameter(typeof(TInstance)); 

     var parameterExpression = Expression.Parameter(typeof(TProperty), propertyName); 

     var propertyValueExpression = Expression.Property(instanceParameterExpression, propertyName); 

     var conversionExpression = propertyValueExpression.Type == typeof(TProperty) ? parameterExpression : (Expression)Expression.Convert(parameterExpression, propertyValueExpression.Type); 

     var propertySetter = Expression.Lambda<Action<TInstance, TProperty>>(Expression.Assign(propertyValueExpression, conversionExpression), instanceParameterExpression, parameterExpression).Compile(); 

     return propertySetter; 
    } 
} 
+0

リリースを使用して結果を実行しましたか?結果はあなたを驚かせるかもしれません。 – Svek

+0

私はまた、アクションを回避するのではなく、ベンチマークを個々のメソッドに単純化したいと思うかもしれません。私はJITの専門家ではありませんが、それはそれが何かを持っていると思われます。 – Svek

+0

@Svek実際にはリリースビルドを試してみましたが、直感的には直感的でしたが、比は約15倍遅くなりました。ネイティブアクセスのステップを実行するのに費やされた平均時間は、キャッシュされたコンパイル済みのラムダ式のwasterは少し遅くなります。デリゲートでラップアップすることについては、少し遅くなりますが、比率に関しては、このようにしてもしなくても一貫していなければなりません。 – Ehouarn

答えて

0

にそれを実行するためのプログラムを使用している、問題がで常駐ベンチマークが達成される方法。実際のところパフォーマンス面では、ヘルパーや拡張メソッド、特に辞書ルックアップ操作に関するすべてが重要です。

コンパイルされたラムダそのものの結果の実行と比較して、ルックアップ操作のために辞書が(定数O(1)であっても)時間がはっきりと過小評価されています。それはコンパイルされたラムダ公演のパフォーマンスの後に私が元々あった理由です)。

質問のコメントに記載されているとおり、はい私は結果をキャッシュすることができます。その場合、ネイティブプロパティへのアクセスが本当に近づいています。拡張メソッドは本当に便利ですが、それは本当に重要なディテールを隠してしまったので、ベンチマークの方法が本当に大丈夫ではないと私が言った理由です。ここで

は私の質問についての問題のいくつかの光を当てる完全なコードを下回っている:

public static class Program 
{ 
    public static void Main(params string[] args) 
    { 
     var stepCount = 5000000UL; 

     var dummy = new Dummy(); 

     const string propertyName = "Soother"; 

     const bool propertyValue = true; 

     var propertyInfo = typeof(Dummy).GetProperty(propertyName); 

     var lambdaExpression = FastPropertyFactory.GeneratePropertySetter<Dummy, bool>(propertyName); 

     var nativeBenchmark = Benchmark.Run("Native", stepCount,() => dummy.Soother = propertyValue); 
     var lambdaExpressionBenchmark = Benchmark.Run("Lambda Expression", stepCount,() => lambdaExpression(dummy, propertyValue)); 
     var dictionaryLambdaExpressionBenchmark = Benchmark.Run("Dictionary Access + Lambda Expression", stepCount,() => dummy.Set(propertyName, propertyValue)); 
     var propertyInfoBenchmark = Benchmark.Run("Property Info", stepCount,() => propertyInfo.SetValue(dummy, propertyValue, null)); 

     var benchmarkReports = new[] 
     { 
      nativeBenchmark, 
      lambdaExpressionBenchmark, 
      dictionaryLambdaExpressionBenchmark, 
      propertyInfoBenchmark 
     }.OrderBy(item => item.ElapsedMilliseconds); 

     benchmarkReports.Join(Environment.NewLine).WriteLineToConsole(); 

     var fastest = benchmarkReports.First().ElapsedMilliseconds; 

     benchmarkReports.Select(report => (report.ElapsedMilliseconds/fastest).ToString("0.000")).Join(" < ").WriteLineToConsole(); 

     var dictionaryAccess = (dictionaryLambdaExpressionBenchmark.ElapsedMilliseconds/lambdaExpressionBenchmark.ElapsedMilliseconds * 100); 
     ("Dictionary Access: " + dictionaryAccess + " %").WriteLineToConsole(); 

     Console.ReadKey(); 
    } 
} 

public class Dummy 
{ 
    public Dummy(bool soother = true) 
    { 
     Soother = soother; 
    } 

    public bool? Soother { get; set; } 
} 

public class BenchMarkReport 
{ 
    #region Fields & Properties 

    public string Name { get; } 

    public TimeSpan ElapsedTime { get; } 

    public double ElapsedMilliseconds => ElapsedTime.TotalMilliseconds; 

    public ulong StepCount { get; } 

    public double StepElapsedMilliseconds => ElapsedMilliseconds/StepCount; 

    #endregion 

    #region Constructors 

    internal BenchMarkReport(string name, TimeSpan elapsedTime, ulong stepCount) 
    { 
     Name = name; 
     ElapsedTime = elapsedTime; 
     StepCount = stepCount; 
    } 

    #endregion 

    #region Methods 

    public override string ToString() 
    { 
     return $"{Name}: Elapsed = {ElapsedTime} ({ElapsedMilliseconds} ms); Step = {StepElapsedMilliseconds:0.###E+000} ms"; 
    } 

    #endregion 
} 

public class Benchmark 
{ 
    #region Fields & Properties 

    private readonly Action _stepAction; 

    public string Name { get; } 

    public ulong StepCount { get; } 

    public Benchmark(string name, ulong stepCount, Action stepAction) 
    { 
     Name = name; 
     StepCount = stepCount; 
     _stepAction = stepAction; 
    } 

    #endregion 

    #region Constructors 

    #endregion 

    #region Methods 

    public static BenchMarkReport Run(string name, ulong stepCount, Action stepAction) 
    { 
     var benchmark = new Benchmark(name, stepCount, stepAction); 

     var benchmarkReport = benchmark.Run(); 

     return benchmarkReport; 
    } 

    public BenchMarkReport Run() 
    { 
     return Run(StepCount); 
    } 

    public BenchMarkReport Run(ulong stepCountOverride) 
    { 
     var stopwatch = Stopwatch.StartNew(); 

     for (ulong i = 0; i < StepCount; i++) 
     { 
      _stepAction(); 
     } 

     stopwatch.Stop(); 

     var benchmarkReport = new BenchMarkReport(Name, stopwatch.Elapsed, stepCountOverride); 

     return benchmarkReport; 
    } 

    #endregion 
} 

public static class ObjectExtensions 
{ 
    public static void WriteToConsole<TInstance>(this TInstance instance) 
    { 
     Console.Write(instance); 
    } 

    public static void WriteLineToConsole<TInstance>(this TInstance instance) 
    { 
     Console.WriteLine(instance); 
    } 

    // Goodies: add name inference from property lambda expression 
    // e.g. "instance => instance.PropertyName" redirected using "PropertyName" 

    public static TProperty Get<TInstance, TProperty>(this TInstance instance, string propertyName) 
    { 
     return FastPropertyRepository<TInstance, TProperty>.GetGetter(propertyName)(instance); 
    } 

    public static void Set<TInstance, TProperty>(this TInstance instance, string propertyName, TProperty propertyValue) 
    { 
     FastPropertyRepository<TInstance, TProperty>.GetSetter(propertyName)(instance, propertyValue); 
    } 
} 

public static class EnumerableExtensions 
{ 
    public static string Join<TSource>(this IEnumerable<TSource> source, string separator = ", ") 
    { 
     return string.Join(separator, source); 
    } 
} 

internal static class FastPropertyRepository<TInstance, TProperty> 
{ 
    private static readonly IDictionary<string, Action<TInstance, TProperty>> Setters; 
    private static readonly IDictionary<string, Func<TInstance, TProperty>> Getters; 

    static FastPropertyRepository() 
    { 
     Getters = new ConcurrentDictionary<string, Func<TInstance, TProperty>>(); 
     Setters = new ConcurrentDictionary<string, Action<TInstance, TProperty>>(); 
    } 

    public static Func<TInstance, TProperty> GetGetter(string propertyName) 
    { 
     if (!Getters.TryGetValue(propertyName, out Func<TInstance, TProperty> getter)) 
     { 
      getter = FastPropertyFactory.GeneratePropertyGetter<TInstance, TProperty>(propertyName); 
      Getters[propertyName] = getter; 
     } 

     return getter; 
    } 

    public static Action<TInstance, TProperty> GetSetter(string propertyName) 
    { 
     if (!Setters.TryGetValue(propertyName, out Action<TInstance, TProperty> setter)) 
     { 
      setter = FastPropertyFactory.GeneratePropertySetter<TInstance, TProperty>(propertyName); 
      Setters[propertyName] = setter; 
     } 

     return setter; 
    } 
} 

internal static class FastPropertyFactory 
{ 
    public static Func<TInstance, TProperty> GeneratePropertyGetter<TInstance, TProperty>(string propertyName) 
    { 
     var parameterExpression = Expression.Parameter(typeof(TInstance), "value"); 

     var propertyValueExpression = Expression.Property(parameterExpression, propertyName); 

     var expression = propertyValueExpression.Type == typeof(TProperty) ? propertyValueExpression : (Expression)Expression.Convert(propertyValueExpression, typeof(TProperty)); 

     var propertyGetter = Expression.Lambda<Func<TInstance, TProperty>>(expression, parameterExpression).Compile(); 

     return propertyGetter; 
    } 

    public static Action<TInstance, TProperty> GeneratePropertySetter<TInstance, TProperty>(string propertyName) 
    { 
     var instanceParameterExpression = Expression.Parameter(typeof(TInstance)); 

     var parameterExpression = Expression.Parameter(typeof(TProperty), propertyName); 

     var propertyValueExpression = Expression.Property(instanceParameterExpression, propertyName); 

     var conversionExpression = propertyValueExpression.Type == typeof(TProperty) ? parameterExpression : (Expression)Expression.Convert(parameterExpression, propertyValueExpression.Type); 

     var propertySetter = Expression.Lambda<Action<TInstance, TProperty>>(Expression.Assign(propertyValueExpression, conversionExpression), instanceParameterExpression, parameterExpression).Compile(); 

     return propertySetter; 
    } 
} 

そして例のために、ここで私のマシン上の結果を下回っている:

Native: Elapsed = 00:00:00.1346658 (134.6658 ms); Step = 2.693E-005 ms 
Lambda Expression: Elapsed = 00:00:00.1578168 (157.8168 ms); Step = 3.156E-005 ms 
Dictionary Access + Lambda Expression: Elapsed = 00:00:00.8092977 (809.2977 ms); Step = 1.619E-004 ms 
Property Info: Elapsed = 00:00:02.2420732 (2242.0732 ms); Step = 4.484E-004 ms 
1.000 < 1.172 < 6.010 < 16.649 
Dictionary Access: 512.80833219277 % 
1

私はあなたの仕事をより小さな方法に簡略化しました。パフォーマンスは全体的に向上しましたが、ギャップも広がりました。ここで

Native    : 00:00:00.0029713 ( 2.9713ms) 5.9426E-07 
Lambda Expression : 00:00:00.4356385 ( 435.6385ms) 8.71277E-05 
Property Info  : 00:00:01.3436626 (1343.6626ms) 0.00026873252 

方法は

public class Dummy 
{ 
    public bool? Soother { get; set; } = true; 
} 

public class Lab 
{ 
    Dummy _dummy = new Dummy(); 
    ulong _iterations = 5000000UL; 
    const bool _propertyValue = true; 
    const string _propertyName = "Soother"; 

    public BenchmarkReport RunNative() 
    { 
     Stopwatch stopwatch = Stopwatch.StartNew(); 
     for (ulong i = 0; i < _iterations; i++) 
     { 
      _dummy.Soother = _propertyValue; 
     } 
     stopwatch.Stop(); 

     return new BenchmarkReport("Native", stopwatch.Elapsed, _iterations); 
    } 

    public BenchmarkReport RunLambdaExpression() 
    { 
     Stopwatch stopwatch = Stopwatch.StartNew(); 
     for (ulong i = 0; i < _iterations; i++) 
     { 
      _dummy.Set(_propertyName, _propertyValue); 
     } 
     stopwatch.Stop(); 

     return new BenchmarkReport("Lambda Expression", stopwatch.Elapsed, _iterations); 
    } 

    public BenchmarkReport RunPropertyInfo() 
    { 
     PropertyInfo propertyInfo = typeof(Dummy).GetProperty(_propertyName); 

     Stopwatch stopwatch = Stopwatch.StartNew(); 
     for (ulong i = 0; i < _iterations; i++) 
     { 
      propertyInfo.SetValue(_dummy, _propertyValue); 
     } 
     stopwatch.Stop(); 

     return new BenchmarkReport("Property Info", stopwatch.Elapsed, _iterations); 
    } 
} 

public class BenchmarkReport 
{ 
    public string Name { get; set; } 
    public TimeSpan ElapsedTime { get; set; } 
    public ulong Iterations { get; set; } 

    public BenchmarkReport(string name, TimeSpan elapsedTime, ulong iterations) 
    { 
     Name = name; 
     ElapsedTime = elapsedTime; 
     Iterations = iterations; 
    } 
} 

と私の質問以下のコメントのやりとりで述べたように

public static class Program 
{ 
    public static void Main(params string[] args) 
    { 

     Lab lab = new Lab(); 
     List<BenchmarkReport> benchmarkReports = new List<BenchmarkReport>() 
     { 
      lab.RunNative(), 
      lab.RunLambdaExpression(), 
      lab.RunPropertyInfo() 
     }; 

     foreach (var report in benchmarkReports) 
     { 
      Console.WriteLine("{0}: {1} ({2}ms) {3}", 
       report.Name.PadRight(20), 
       report.ElapsedTime, 
       report.ElapsedTime.TotalMilliseconds.ToString().PadLeft(10), 
       (double)report.ElapsedTime.TotalMilliseconds/report.Iterations); 
     } 

     Console.ReadKey(); 
    } 
} 
関連する問題