编译C#lambda表达性能与重叠

Mic*_*iti 15 c# linq lambda expression-trees

考虑这个课程:

/// <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";
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Parser parser = new Parser();
List<int> list = parser.ReadList(parser.ReadInt32);
Run Code Online (Sandbox Code Playgroud)

但是,表现并不完全相同......

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这些是我的计算机上以毫秒为单位的结果:

Direct call:          647 ms
Lambda call:          641 ms
Compiled lambda call: 5861 ms
Run Code Online (Sandbox Code Playgroud)

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

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

更新:改变标杆基于DateTime.NowStopwatch.

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

Ani*_*Ani 13

测试无效有两个原因:

DateTime.Now 对于微测试短测试来说还不够准确.

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

Lambda call: 86.3196
Direct call: 74.057
Compiled lambda call: 814.2178
Run Code Online (Sandbox Code Playgroud)

实际上,"直接调用"比"lambda调用"更快,这是有道理的 - "直接调用"涉及对委托的调用,该委托直接引用Parser对象上的方法."lambda调用"需要调用委托,该委托引用编译器生成的闭包对象上的方法,该Parser对象又调用该对象上的方法.这种额外的间接引入了一个小的速度碰撞.


"编译的lambda调用"与"Lambda调用"不同

"Lambda"看起来像这样:

() => parser.ReadInt32()
Run Code Online (Sandbox Code Playgroud)

而"Compiled lambda"看起来像这样:

parser => parser.ReadList(() => parser.ReadInt32())
Run Code Online (Sandbox Code Playgroud)

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

编辑:

我继续检查IL的"lambda"与"编译的lambda"并将它们反编译成"更简单"的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);
}
Run Code Online (Sandbox Code Playgroud)

需要注意的是一个单一的委托创建一次和缓存.

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

Func<Parser, List<int>> func = lambda.Compile();
Parser parser = new Parser();
for (int i = 0; i < 100000; i++)
{
    func(parser);
}
Run Code Online (Sandbox Code Playgroud)

代表的目标是:

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));
}
Run Code Online (Sandbox Code Playgroud)

请注意,虽然"外部"委托只创建一次并缓存,但是在循环的每次迭代中都会创建一个新的"内部"委托.更不用说object数组和StrongBox<T>实例的其他分配.


Jef*_*Sax 7

  1. 编译的lambda较慢的主要原因是因为委托是一次又一次地创建的.匿名代表是一种特殊的代表:它们只在一个地方使用.因此编译器可以进行一些特殊的优化,比如在第一次调用委托时缓存该值.这就是这里发生的事情.

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

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

此外,三个案件中的第一个将导致JIT Parser课堂上的轻微开销.尝试两次运行第一个案例,看看会发生什么.或者更好的是:在每个方法中使用迭代次数作为参数,并首先调用每个方法1次迭代,因此它们都在一个级别的游戏场上开始.