编译的C# lambda表达式在嵌套时的性能表现

15
考虑以下这个类:
/// <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调用如此缓慢。

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

更新:基于Stopwatch更改了基准测试,不再使用DateTime.Now

有人知道如何调整lambda表达式以获得更好的编译后lambda调用性能吗?


2
欢迎来到stackoverflow - 我们会喜欢你的。 - Connell
2个回答

13

这个测试有两个问题:

DateTime.Now 不够准确,不能用于微基准测试短时间的测试。

应该使用 Stopwatch 类替代。当我使用 Stopwatch 类时,在毫秒级别下得到以下结果(使用 MAX = 100000):

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

的确,“直接调用”比“Lambda调用”更快,这很合理 - “直接调用”涉及对委托的调用,该委托直接引用Parser对象上的方法。“Lambda调用”需要调用一个委托,该委托引用编译器生成的闭包对象上的方法,然后调用Parser对象上的方法。这种额外的间接性引入了轻微的速度障碍。


“已编译Lambda调用”与“Lambda调用”并不相同

“Lambda”看起来像这样:

() => parser.ReadInt32()

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

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

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

编辑

我已经检查了“lambda”与“已编译的lambda”的IL,并将它们反编译回“更简单”的C#(请参见:查看从编译表达式生成的IL代码)。

对于“未编译”的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>{1}lambda_method));
}

请注意,尽管“外部”委托只创建一次并缓存,但在循环的每次迭代中都会创建一个新的“内部”委托。更不用说为对象数组和实例进行的其他分配了。


但是外部 lambda 表达式会提前编译为委托,内部 lambda 表达式在其他情况下也存在。 - Jeffrey Sax
谢谢Ani!你知道是否有一种不同的方式来表达我的LambdaExpression以获得相同类型的性能吗?(我更新了我的问题) - Michaël Catanzariti

7
  1. 编译后的lambda表达式较慢的主要原因是委托会反复创建。匿名委托是一种特殊的类型:它们只在一个位置使用。因此,编译器可以进行一些特殊的优化,例如缓存第一次调用委托时的值。这就是这里发生的情况。

  2. 我无法重现直接调用和lambda调用之间的大差异。实际上,在我的测试中,直接调用略快。

在进行类似的基准测试时,您可能需要使用更精确的计时器。System.Diagnostics中的Stopwatch类是理想的选择。您还可以增加迭代次数。代码只运行了几毫秒。

此外,三种情况中的第一种情况将从JIT'ing Parser 类中产生轻微的开销。尝试两次运行第一个案例,并查看会发生什么。或者更好的方法是:将迭代次数作为每个方法的参数,并首先为每个方法调用1次,以便它们都在同一水平线上开始。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接