已编译的 lambda 表达式用作 属性 getter 和 setter:错误的基准测试方法或错误的 lambda 表达式构造?

Compiled lambda expressions used as property getters and setters: wrong benchmarking method or wrong lambda expression construction?

您好,我正在尝试使用(缓存的)已编译的 lambda 表达式进行属性访问,与使用 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

老实说,我觉得根据其他基准,编译的 lambda 表达式应该比使用常规属性慢两倍,而不是慢 5 到 6 倍。

有什么想法吗?基准测试方法?编译后的lambda表达式的计算方式?

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;
    }
}

我将您的工作简化为更小的方法。它提高了整体性能,但也拉大了差距。

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();
    }
}

正如我在问题下方的评论交流中所述,问题在于实现基准的方式。实际上底线 performance-wise 都是关于帮助器和扩展方法的,尤其是字典查找操作。

与执行已编译 lambda 本身的结果相比,我显然低估了字典查找操作所需的时间(即使它是常量 O(1)),也就是说它仍然慢了 lo0o0ot (是的,这就是为什么我最初在编译的 lambda 表演之后那么多的原因)。

如问题评论中所述,是的,我可以缓存结果,在这种情况下,我获得的性能非常接近本机 属性 访问。扩展方法真的很方便,但是它确实隐藏了一个非常重要的细节,因此我说 bench-marking 的方式不太好。

下面是完整代码,阐明了我的问题的一些问题:

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 %