Lambda函数比委托/匿名函数更快吗?

12

我假设使用相同代码体的lambda函数委托匿名函数的速度是相同的,但是运行下面这个简单的程序后:

static void Main(string[] args)
{
    List<int> items = new List<int>();

    Random random = new Random();

    for (int i = 0; i < 10000000; i++)
    {
        items.Add(random.Next());
    }

    Stopwatch watch;
    IEnumerable<int> result;

    Func<int, bool> @delegate = delegate(int i)
    {
        return i < 500;
    };
    watch = Stopwatch.StartNew();
    result = items.Where(@delegate);
    watch.Stop();
    Console.WriteLine("Delegate: {0}", watch.Elapsed.TotalMilliseconds);

    Func<int, bool> lambda = i => i < 500;
    watch = Stopwatch.StartNew();
    result = items.Where(lambda);
    watch.Stop();
    Console.WriteLine("Lambda: {0}", watch.Elapsed.TotalMilliseconds);

    watch = Stopwatch.StartNew();
    result = items.Where(i => i < 500);
    watch.Stop();
    Console.WriteLine("Inline: {0}", watch.Elapsed.TotalMilliseconds);

    Console.ReadLine();
}

我得到:

委托:4.2948毫秒

lambda表达式:0.0019毫秒

匿名方法:0.0034毫秒

虽然微不足道,但为什么这三种 - 表面上相同的 - 方法运行速度不同?在幕后发生了什么?


更新:

根据评论中的建议,以下代码通过调用ToList()来“强制”使用Where。此外,添加了一个循环以提供更多的运行数据:

while (true) 
{
    List<int> items = new List<int>();

    Random random = new Random();

    for (int i = 0; i < 10000000; i++)
    {
        items.Add(random.Next());
    }

    Stopwatch watch;
    IEnumerable<int> result;

    Func<int, bool> @delegate = delegate(int i)
    {
        return i < 500;
    };
    watch = Stopwatch.StartNew();
    result = items.Where(@delegate).ToList();
    watch.Stop();
    Console.WriteLine("Delegate: {0}", watch.Elapsed.TotalMilliseconds);

    Func<int, bool> lambda = i => i < 500;
    watch = Stopwatch.StartNew();
    result = items.Where(lambda).ToList();
    watch.Stop();
    Console.WriteLine("Lambda: {0}", watch.Elapsed.TotalMilliseconds);

    watch = Stopwatch.StartNew();
    result = items.Where(i => i < 500).ToList();
    watch.Stop();
    Console.WriteLine("Inline: {0}", watch.Elapsed.TotalMilliseconds);
    Console.WriteLine(new string('-', 12));

}

以上代码每个函数的执行时间约为120毫秒。


只是出于好奇,你尝试过以不同的顺序运行这三个吗? - kenwarner
5
加入热身时间,并运行所有测试数次循环(围绕所有测试的大循环),并强制执行 Where。在这些更改之后,我没有看到任何明显的差异。 - user166390
1
这段代码中的 @ 符号有什么作用?(@delegate) - Igby Largeman
3
@Omar:你展示的代码肯定不会花费120ms——我建议你编辑代码,以展示你是如何“强制”使用Where的。 - Jon Skeet
1
@Mikael:谢谢 - 我不知道你可以这样做。 - Igby Largeman
显示剩余4条评论
2个回答

21

其他人的结果表明性能是相同的:

http://blogs.microsoft.co.il/blogs/alex_golesh/archive/2007/12/11/anonymous-delegates-vs-lambda-expressions-vs-function-calls-performance.aspx

正如评论中所提到的,微基准测试经常会误导人。有太多因素不受您控制,例如JIT优化、垃圾收集周期等等...

请参见以下相关问题:

何时不应使用lambda表达式

最后,我认为你的测试基本上存在缺陷!你使用Linq Where扩展方法来执行你的代码。然而,Linq使用延迟计算,只有在开始迭代结果时才会执行你的代码!


4
你能否将最后一段加粗显示? :) - leppie

17

一个lambda表达式是一个匿名函数。"匿名函数"可以指lambda表达式或匿名方法(在你的代码中,你称之为"delegate")。

所有三个操作都使用委托。第二个和第三个都使用lambda表达式。所有三个操作将以相同的方式执行,并具有相同的性能特征。

注意,以下两者之间可能存在性能差异:

Func<int, int> func = x => ...;
for (int i = 0; i < 10000; i++) {
    CallFunc(func);
}

并且

for (int i = 0; i < 10000; i++) {
    CallFunc(x => ...) // Same lambda as before
}

这取决于编译器是否能够缓存由lambda表达式创建的委托。这又将取决于它是否捕获变量等。

例如,考虑以下代码:

using System;
using System.Diagnostics;

class Test
{
    const int Iterations = 1000000000;

    static void Main()
    {
        AllocateOnce();
        AllocateInLoop();
    }

    static void AllocateOnce()
    {
        int x = 10;

        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        Func<int, int> allocateOnce = y => y + x;
        for (int i = 0; i < Iterations; i++)
        {
            sum += Apply(i, allocateOnce);
        }
        sw.Stop();
        Console.WriteLine("Allocated once: {0}ms", sw.ElapsedMilliseconds);
    }

    static void AllocateInLoop()
    {
        int x = 10;

        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        for (int i = 0; i < Iterations; i++)
        {
            sum += Apply(i, y => y + x);
        }
        sw.Stop();
        Console.WriteLine("Allocated in loop: {0}ms", sw.ElapsedMilliseconds);
    }

    static int Apply(int loopCounter, Func<int, int> func)
    {
        return func(loopCounter);
    }
}
编译器很聪明,但还是有区别。使用Reflector,我们可以看到AllocateInLoop被编译为以下内容:

编译器智能,但仍存在差异。使用Reflector,我们可以看到AllocateInLoop被有效地编译为:

private static void AllocateInLoop()
{
    Func<int, int> func = null;
    int x = 10;
    Stopwatch stopwatch = Stopwatch.StartNew();
    int sum = 0;
    for (int i = 0; i < Iterations; i++)
    {
        if (func == null)
        {
            func = y => y + x;
        }
        sum += Apply(i, func);
    }
    stopwatch.Stop();
    Console.WriteLine("Allocated in loop: {0}ms", stopwatch.ElapsedMilliseconds);
}

因此仍然只创建了一个委托实例,但是循环内有额外的逻辑——基本上在每次迭代中都进行了额外的nullity测试。

在我的机器上,这会导致性能差异约为15%。


1
我不确定您如何检查IL输出,但我认为delegate { Foo(); }() => { Foo(); }会编译成相同的IL,对吗? - Lea Hayes
2
@LeaHayes:我认为是的,没错。 - Jon Skeet

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