编译后的 C# lambda 表达式性能与覆盖

Posted

技术标签:

【中文标题】编译后的 C# lambda 表达式性能与覆盖【英文标题】:Compiled C# lambda expression performance with imbrication 【发布时间】:2011-12-24 12:15:40 【问题描述】:

考虑这个类:

/// <summary>
/// Dummy implementation of a parser for the purpose of the test
/// </summary>
class Parser

    public List<T> ReadList<T>(Func<T> readFunctor)
    
        return Enumerable.Range(0, 10).Select(i => readFunctor()).ToList();
    

    public int ReadInt32()
    
        return 12;
    

    public string ReadString()
    
        return "string";
    

我尝试使用已编译的 lambda 表达式树生成以下调用:

Parser parser = new Parser();
List<int> list = parser.ReadList(parser.ReadInt32);

但是,性能不太一样...

class Program

    private const int MAX = 1000000;

    static void Main(string[] args)
    
        DirectCall();
        LambdaCall();
        CompiledLambdaCall();
    

    static void DirectCall()
    
        Parser parser = new Parser();
        var sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < MAX; i++)
        
            List<int> list = parser.ReadList(parser.ReadInt32);
        
        sw.Stop();
        Console.WriteLine("Direct call: 0 ms", sw.ElapsedMilliseconds);
    

    static void LambdaCall()
    
        Parser parser = new Parser();
        var sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < MAX; i++)
        
            List<int> list = parser.ReadList(() => parser.ReadInt32());
        
        sw.Stop();
        Console.WriteLine("Lambda call: 0 ms", sw.ElapsedMilliseconds);
    

    static void CompiledLambdaCall()
    
        var parserParameter = Expression.Parameter(typeof(Parser), "parser");

        var lambda = Expression.Lambda<Func<Parser, List<int>>>(
            Expression.Call(
                parserParameter,
                typeof(Parser).GetMethod("ReadList").MakeGenericMethod(typeof(int)),
                Expression.Lambda(
                    typeof(Func<int>),
                    Expression.Call(
                        parserParameter,
                        typeof(Parser).GetMethod("ReadInt32")))),
            parserParameter);
        Func<Parser, List<int>> func = lambda.Compile();

        Parser parser = new Parser();
        var sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < MAX; i++)
        
            List<int> list = func(parser);
        
        sw.Stop();
        Console.WriteLine("Compiled lambda call: 0 ms", sw.ElapsedMilliseconds);
    

这些是我电脑上以毫秒为单位的结果:

Direct call:          647 ms
Lambda call:          641 ms
Compiled lambda call: 5861 ms

我不明白为什么编译的 lambda 调用这么慢。

我忘了说我的测试是在发布模式下运行的,启用了“优化代码”选项。

更新:将基于 DateTime.Now 的基准测试更改为 Stopwatch

有谁知道如何调整 lambda 表达式以在编译的 lambda 调用中获得更好的性能?

【问题讨论】:

欢迎来到 *** - 我们会爱你的 【参考方案1】:

测试无效有两个原因:

DateTime.Now 不够准确,无法进行微型基准测试。

改用Stopwatch 类。当我这样做时,我得到以下结果(使用 MAX = 100000),以毫秒为单位:

Lambda call: 86.3196
Direct call: 74.057
Compiled lambda call: 814.2178

确实,“直接调用”比“lambda 调用”更快,这是有道理的 - “直接调用”涉及对委托的调用,该委托直接引用@987654325 上的方法@ 目的。 “lambda 调用”需要调用一个委托,该委托引用编译器生成的闭包对象上的方法,该闭包对象又调用Parser 对象上的方法。这种额外的间接引入了一个小的减速带。


“编译的 lambda 调用”与“Lambda 调用”不同

“Lambda”看起来像这样:

() => parser.ReadInt32()

而“编译的 lambda”看起来像这样:

parser => parser.ReadList(() => parser.ReadInt32())

还有一个额外的步骤:为内部 lambda 创建嵌入式委托。在一个紧密的循环中,这很昂贵。

编辑

我继续检查了“lambda”与“已编译 lambda”的 IL,并将它们反编译回“更简单”的 C#(请参阅:Viewing the IL code generated from a compiled expression)。

对于“未编译”的 lambda,它看起来像这样:

for (int i = 0; i < 100000; i++)

    if (CS$<>9__CachedAnonymousMethodDelegate1 == null)
    
        CS$<>9__CachedAnonymousMethodDelegate1 = new Func<int>(CS$<>8__locals3.<LambdaCall>b__0);
    

    CS$<>8__locals3.parser.ReadList<int>(CS$<>9__CachedAnonymousMethodDelegate1);

请注意,单个委托被创建一次并被缓存。

而对于“编译的 lambda”,它看起来像这样:

Func<Parser, List<int>> func = lambda.Compile();
Parser parser = new Parser();
for (int i = 0; i < 100000; i++)

    func(parser);

委托的目标是:

public static List<int> Foo(Parser parser)

    object[] objArray = new object[]  new StrongBox<Parser>(parser) ;
    return ((StrongBox<Parser>) objArray[0]).Value.ReadList<int>
      (new Func<int>(dyn_type.<ExpressionCompilerImplementationDetails>1lambda_method));

请注意,虽然“外部”委托只创建一次并被缓存,但在循环的每次迭代中都会创建一个新的“内部”委托。更不用说object 数组和StrongBox&lt;T&gt; 实例的其他分配了。

【讨论】:

但是外部的 lambda 是事先编译为委托的,而内部的 lambda 在其他情况下也存在。 谢谢阿尼!你知道是否有办法以不同的方式表达我的 LabmdaExpression 以获得相同的性能? (我更新了我的问题)【参考方案2】:

    编译后的 lambda 速度较慢的主要原因是委托被反复创建。匿名代表是一个特殊的品种:他们只在一个地方使用。所以编译器可以做一些特殊的优化,比如在第一次调用委托时缓存值。这就是这里发生的事情。

    我无法重现直接调用和 lambda 调用之间的巨大差异。事实上,在我的测量中,直接调用稍微快一些。

在进行此类基准测试时,您可能希望使用更准确的计时器。 System.Diagnostics 中的Stopwatch 类是理想的。您可能还想增加迭代次数。代码只运行几毫秒。

此外,这三种情况中的第一种会因 JIT 处理 Parser 类而产生轻微的开销。尝试运行第一个案例两次,看看会发生什么。或者更好的是:在每个方法中使用迭代次数作为参数,并首先调用每个方法进行 1 次迭代,因此它们都在公平竞争环境中开始。

【讨论】:

以上是关于编译后的 C# lambda 表达式性能与覆盖的主要内容,如果未能解决你的问题,请参考以下文章

C#中 Lambda表达式 与正则表达式的关系

C# 编译器错误:无法转换 lambda 表达式

C# 10 Lambda 语法的改进

C#中的委托,匿名方法和Lambda表达式

编译到委托表达式的性能

C# 委托 —— 委托 泛型委托与Lambda表达式