C#Lambda性能问题/可能性/指南

时间:2011-11-29 02:37:24

标签: c# lambda performance-testing lifting

我正在使用各种lambda表达式语法测试性能差异。如果我有一个简单的方法:

public IEnumerable<Item> GetItems(int point)
{
    return this.items.Where(i => i.IsApplicableFor(point));
}

然后这里有一些与point参数相关的变量提升,因为它是lambda视角的自由变量。如果我将这种方法称为百万次,那么保持原样是否更好或以任何方式改变它以改善其性能?

我有哪些选项以及哪些选项实际可行?据我所知,我必须摆脱自由变量,因此编译器不必创建闭包类并在每个实例化它打电话给这个方法。与非闭包版本相比,此实例化通常需要大量时间。

问题是我想提出一些通常可行的 lambda写作指南,因为看起来我每次写一个受到重创的lambda表达式时都会浪费一些时间。我必须手动测试它以确保它能够正常工作,因为我不知道应遵循哪些规则。

替代方法

&安培;示例控制台应用程序代码

我还写了一个不同版本的相同方法,不需要任何变量提升(至少我认为它没有,但是你理解这个的人让我知道是否是这种情况):

public IEnumerable<Item> GetItems(int point)
{
    Func<int, Func<Item, bool>> buildPredicate = p => i => i.IsApplicableFor(p);
    return this.items.Where(buildPredicate(point));
}

结帐Gist here。只需创建一个控制台应用程序并将整个代码复制到Program.cs块内的namespace文件中。您将看到第二个示例即使不使用自由变量也要慢得多。

一个矛盾的例子

我想构建一些 lambda最佳使用指南的原因是我met this problem before,令我惊讶的是,当时,一个人的工作速度更快使用了谓词构建器 lambda表达式。

现在解释一下。我完全迷失在这里,因为当我知道我的代码中有一些繁重的使用方法时,我可能根本不会使用lambdas。但我想避免这种情况,并深究这一切。

修改

您的建议似乎不起作用

我尝试实现一个自定义查找类,其内部工作方式类似于编译器对自由变量lambda的操作。但是,我没有使用闭包类,而是实现了模拟类似场景的实例成员。这是代码:

private int Point { get; set; }
private bool IsItemValid(Item item)
{
    return item.IsApplicableFor(this.Point);
}

public IEnumerable<TItem> GetItems(int point)
{
    this.Point = point;
    return this.items.Where(this.IsItemValid);
}

有趣的是,这种方法与慢速版本一样慢。我不知道为什么,但似乎除了速度之外什么也没做。它重用了相同的功能,因为这些附加成员是同一对象实例的一部分。无论如何。 我现在非常困惑

我已使用此最新添加内容更新了Gist source,因此您可以自行测试。

4 个答案:

答案 0 :(得分:3)

是什么让你认为第二个版本不需要任何变量提升?您使用Lambda表达式定义Func,这将需要与第一个版本相同的编译器技巧。

此外,您正在创建一个返回Func的{​​{1}},这会使我的大脑稍微弯曲,几乎肯定需要在每次通话时重新评估。

我建议您在发布模式下编译它,然后使用ILDASM检查生成的IL。这应该可以让您深入了解生成的代码。

您应该运行的另一个测试,它将为您提供更多的见解,是使谓词调用成为在类范围内使用变量的单独函数。类似的东西:

Func

这将告诉您是否提升private DateTime dayToCompare; private bool LocalIsDayWithinRange(TItem i) { return i.IsDayWithinRange(dayToCompare); } public override IEnumerable<TItem> GetDayData(DateTime day) { dayToCompare = day; return this.items.Where(i => LocalIsDayWithinRange(i)); } 变量实际上是否会使您付出任何代价。

是的,这需要更多代码,我不建议您使用它。正如您在对先前回答中提出类似建议的回答中指出的那样,这会产生相当于使用局部变量的闭包。关键是你或者编译器必须做这样的事情才能使事情有效。除了编写纯迭代解决方案之外,没有任何魔法可以阻止编译器执行此操作。

我的观点是,在我的案例中“创建闭包”是一个简单的变量赋值。如果这比使用Lambda表达式的版本快得多,那么您就知道编译器为闭包创建的代码效率低下。

我不确定您在哪里获取有关必须消除自由变量和关闭成本的信息。你能给我一些参考资料吗?

答案 1 :(得分:1)

你的第二种方法比第一种方法慢了8倍。正如@DanBryant在评论中所说,这与在方法中构造和调用委托有关 - 而不是对变量提升。

你的问题令人困惑,因为它向你读取,就像你期望第二个样本比第一个样本更快。我也读过它,因为第一个因为“可变提升”而以某种方式慢得令人无法接受。第二个样本仍然有一个自由变量(point),但它增加了额外的开销 - 我不明白为什么你认为它会删除自由变量。

当您发布的代码确认时,上面的第一个示例(使用简单的内联谓词)比简单的for循环执行jsut慢10% - 来自您的代码:

foreach (TItem item in this.items)
{
    if (item.IsDayWithinRange(day))
    {
        yield return item;
    }
}

所以,总结一下:

  • for循环是最简单的方法,也是“最佳情况”。
  • 由于一些额外的开销,内联谓词稍微较慢。
  • 构建和调用在每次迭代中返回Func的{​​{1}}要慢得多。

我认为这并不令人惊讶。 '准则'是使用内联谓词 - 如果它表现不佳,可以通过直接循环进行简化。

答案 2 :(得分:0)

我为您描述了您的基准并确定了很多事情:

首先,它将一半的时间花在调用return this.GetDayData(day).ToList();的{​​{1}}行上。如果删除它而是手动迭代结果,则可以测量相对方法的差异。

其次,因为ToListIterationCount = 1000000,您正在计算不同方法的初始化,而不是执行它们所花费的时间。这意味着您的执行配置文件由创建迭代器,转义变量记录和委托以及由于创建所有垃圾而产生的数百个后续gen0垃圾收集主导。

第三,“慢”方法在x86上确实很慢,但与x64上的“快速”方法一样快。我相信这是由于不同的JITters如何创建代表。如果从结果中对代理创建进行折扣,则“快速”和“慢速”方法的速度相同。

第四,如果您实际调用迭代器很多次(在我的计算机上,目标是x64,RangeCount = 1),“慢”实际上比“foreach”更快,“fast”比所有更快他们。

总之,“提升”方面可以忽略不计。在我的笔记本电脑上进行测试表明,每次创建lambda时,捕获变量都需要额外的 10ns (每次调用时),这包括额外的GC开销。此外,虽然创建,但是“foreach”方法中的迭代器比创建lambdas要快一些,实际上调用迭代器比调用lambdas要慢。

如果为您的应用程序创建委托所需的额外纳秒数太多,请考虑缓存它们。如果您需要这些委托的参数(即闭包),请考虑创建自己的闭包类,以便您可以创建它们一次,然后在需要重用其委托时更改属性。这是一个例子:

RangeCount = 8

答案 3 :(得分:0)

当使用延迟执行的LINQ表达式在包含它引用的自由变量的同一范围内执行时,编译器应该检测到它而不是在lambda上创建一个闭包,因为它不需要。

验证这种方法的方法是使用以下方法进行测试:

public class Test
{
   public static void ExecuteLambdaInScope()
   {
      // here, the lambda executes only within the scope
      // of the referenced variable 'add'

      var items = Enumerable.Range(0, 100000).ToArray();

      int add = 10;  // free variable referenced from lambda

      Func<int,int> f = x => x + add;

      // measure how long this takes:
      var array = items.Select( f ).ToArray();  
   }

   static Func<int,int> GetExpression()
   {
      int add = 10;
      return x => x + add;  // this needs a closure
   }

   static void ExecuteLambdaOutOfScope()
   {
      // here, the lambda executes outside the scope
      // of the referenced variable 'add'

      Func<int,int> f = GetExpression();

      var items = Enumerable.Range(0, 100000).ToArray();

      // measure how long this takes:
      var array = items.Select( f ).ToArray();  
   }

}