通过对 .Net6 和 .Net7 反射性能的简要测试,为性能优化提供依据。本文测试了以下项目:

名称内容
ReadPub读取公开属性
ReadPriByReflection通过反射读取私有属性
ReadPriByCachedReflection通过缓存的反射读取私有属性
ReadPriByDelegate通过委托读取私有属性
ReadPriByCachedDelegate通过缓存的委托读取私有属性
ReadPriByExpression通过表达式树读取私有属性
ReadPriByCachedExpression通过缓存的表达式树读取私有属性

源代码:DotVast.Benchmark.Reflection/ReadProperty

测试代码片段

C#
using System.Linq.Expressions;
using System.Reflection;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;

namespace DotVast.Benchmark.Reflection;

internal class Program
{
    static void Main(string[] args)
    {
        BenchmarkRunner.Run<ReadProperty>();
    }
}

[SimpleJob(runtimeMoniker: RuntimeMoniker.Net60)]
[SimpleJob(runtimeMoniker: RuntimeMoniker.Net70, baseline: true)]
[MemoryDiagnoser]
public class ReadProperty
{
    private readonly User _user = new();

    [Benchmark]
    public string ReadPub()
    {
        return _user.Pub;
    }

    ///////////////////////////////////////////////////////////////////////////////////
    /// Reflection

    [Benchmark]
    public string ReadPriByReflection()
    {
        var propertyInfo = typeof(User).GetProperty("Pri", BindingFlags.Instance | BindingFlags.NonPublic)!;
        return (string)propertyInfo.GetValue(_user)!;
    }

    static readonly PropertyInfo _propInfo = typeof(User).GetProperty("Pri", BindingFlags.Instance | BindingFlags.NonPublic)!;

    [Benchmark]
    public string ReadPriByCachedReflection()
    {
        return (string)_propInfo.GetValue(_user)!;
    }

    ///////////////////////////////////////////////////////////////////////////////////
    /// Delegate

    [Benchmark]
    public string ReadPriByDelegate()
    {
        var propertyInfo = typeof(User).GetProperty("Pri", BindingFlags.Instance | BindingFlags.NonPublic)!;
        var delFunc = (Func<User, string>)Delegate.CreateDelegate(typeof(Func<User, string>), propertyInfo.GetMethod!);
        return delFunc(_user);
    }

    static readonly Func<User, string> _delFunc = (Func<User, string>)Delegate.CreateDelegate(typeof(Func<User, string>), _propInfo.GetMethod!);

    [Benchmark]
    public string ReadPriByCachedDelegate()
    {
        return _delFunc(_user);
    }

    ///////////////////////////////////////////////////////////////////////////////////
    /// Expression

    [Benchmark]
    public string ReadPriByExpression()
    {
        var propertyInfo = typeof(User).GetProperty("Pri", BindingFlags.Instance | BindingFlags.NonPublic)!;
        var parameter = Expression.Parameter(typeof(User));
        var propertyExpression = Expression.Property(parameter, propertyInfo);
        var expFunc = Expression.Lambda<Func<User, string>>(propertyExpression, parameter).Compile();
        return expFunc(_user);
    }

    static readonly ParameterExpression _parameter = Expression.Parameter(typeof(User));
    static readonly MemberExpression _propExpression = Expression.Property(_parameter, _propInfo);
    static readonly Func<User, string> _expFunc = Expression.Lambda<Func<User, string>>(_propExpression, _parameter).Compile();

    [Benchmark]
    public string ReadPriByCachedExpression()
    {
        return _expFunc(_user);
    }
}

public sealed class User
{
    public  string Pub => "Public";
    private string Pri => "Private";
}

结果

BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19045.2251)
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET SDK=7.0.100
  [Host]   : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2
  .NET 6.0 : .NET 6.0.11 (6.0.1122.52304), X64 RyuJIT AVX2
  .NET 7.0 : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2


|                    Method |      Job |  Runtime |           Mean |       Error |      StdDev | Ratio |   Gen0 |   Gen1 | Allocated | Alloc Ratio |
|-------------------------- |--------- |--------- |---------------:|------------:|------------:|------:|-------:|-------:|----------:|------------:|
|                   ReadPub | .NET 6.0 | .NET 6.0 |      0.4791 ns |   0.0086 ns |   0.0080 ns |  0.34 |      - |      - |         - |          NA |
|                   ReadPub | .NET 7.0 | .NET 7.0 |      1.3930 ns |   0.0069 ns |   0.0061 ns |  1.00 |      - |      - |         - |          NA |
|                           |          |          |                |             |             |       |        |        |           |             |
|       ReadPriByReflection | .NET 6.0 | .NET 6.0 |     94.9653 ns |   0.4266 ns |   0.3782 ns |  2.13 |      - |      - |         - |          NA |
|       ReadPriByReflection | .NET 7.0 | .NET 7.0 |     44.5251 ns |   0.0936 ns |   0.0876 ns |  1.00 |      - |      - |         - |          NA |
|                           |          |          |                |             |             |       |        |        |           |             |
| ReadPriByCachedReflection | .NET 6.0 | .NET 6.0 |     61.6544 ns |   0.1326 ns |   0.1240 ns |  4.21 |      - |      - |         - |          NA |
| ReadPriByCachedReflection | .NET 7.0 | .NET 7.0 |     14.6585 ns |   0.0218 ns |   0.0204 ns |  1.00 |      - |      - |         - |          NA |
|                           |          |          |                |             |             |       |        |        |           |             |
|         ReadPriByDelegate | .NET 6.0 | .NET 6.0 |    452.9171 ns |   2.9943 ns |   2.8008 ns |  1.07 | 0.0200 |      - |      64 B |        1.00 |
|         ReadPriByDelegate | .NET 7.0 | .NET 7.0 |    422.1861 ns |   0.7520 ns |   0.6666 ns |  1.00 | 0.0200 |      - |      64 B |        1.00 |
|                           |          |          |                |             |             |       |        |        |           |             |
|   ReadPriByCachedDelegate | .NET 6.0 | .NET 6.0 |      1.8512 ns |   0.0082 ns |   0.0077 ns |  0.61 |      - |      - |         - |          NA |
|   ReadPriByCachedDelegate | .NET 7.0 | .NET 7.0 |      3.0252 ns |   0.0102 ns |   0.0090 ns |  1.00 |      - |      - |         - |          NA |
|                           |          |          |                |             |             |       |        |        |
|       ReadPriByExpression | .NET 6.0 | .NET 6.0 | 47,972.7743 ns | 273.5695 ns | 255.8971 ns |  0.84 | 1.4038 | 0.6714 |    4551 B |        0.99 |
|       ReadPriByExpression | .NET 7.0 | .NET 7.0 | 57,030.6131 ns | 192.3719 ns | 170.5327 ns |  1.00 | 1.4648 | 1.0376 |    4583 B |        1.00 |
|                           |          |          |                |             |             |       |        |        |           |             |
| ReadPriByCachedExpression | .NET 6.0 | .NET 6.0 |      1.1444 ns |   0.0163 ns |   0.0136 ns |  0.62 |      - |      - |         - |          NA |
| ReadPriByCachedExpression | .NET 7.0 | .NET 7.0 |      1.8552 ns |   0.0073 ns |   0.0068 ns |  1.00 |      - |      - |         - |          NA |

结果总结如下:

  • 通过表达式树访问属性,除直接访问属性外,其性能最佳,但是开销较大,且编写繁琐。
  • 通过反射访问属性,在 .Net7 中优化显著,同时利用缓存也可以获得较好的性能。
  • 通过委托访问属性,其性能和开销较为均衡,适合大多数性能优化使用。