28

我正在使用各种 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));
}

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

一个矛盾的例子

我想构建一些lambda 最佳使用指南的原因是我以前遇到过这个问题,令我惊讶的是,当使用谓词构建器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 源,所以你可以自己测试。

4

4 回答 4

3

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

此外,您正在创建一个Func返回 a 的 a Func,这让我的大脑有点弯曲,几乎可以肯定每次调用都需要重新评估。

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

您应该运行的另一个测试,它将给您更多的洞察力,是使谓词调用一个单独的函数,该函数在类范围内使用一个变量。就像是:

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));
}

这将告诉您提升day变量是否实际上会花费您任何东西。

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

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

我不确定您从哪里获得有关必须消除自由变量以及关闭成本的信息。你能给我一些参考吗?

于 2011-11-29T04:19:18.687 回答
1

对我来说,您的第二种方法比第一种方法运行速度慢 8 倍。正如@DanBryant 在评论中所说,这与在方法内构造和调用委托有关 - 与变量提升无关。

您的问题让我感到困惑,就像您期望第二个样本比第一个样本快一样。我还读了它,因为由于“可变提升”,第一个速度慢得让人无法接受。第二个示例仍然有一个自由变量 ( point),但它增加了额外的开销 - 我不明白你为什么认为它会删除自由变量。

for正如您发布的代码所证实的那样,上面的第一个示例(使用简单的内联谓词)执行的速度比简单循环慢 10% - 从您的代码:

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

所以,总结一下:

  • for循环是最简单的方法,是“最佳情况” 。
  • 由于一些额外的开销,内联谓词稍慢。
  • 构造和调用在每次迭代Func中返回的 aFunc都比任何一个都慢得多。

我认为这并不令人惊讶。“指导方针”是使用内联谓词 - 如果它表现不佳,请通过移动到直循环来简化。

于 2011-11-29T04:49:30.630 回答
0

When a LINQ expression that uses deferred execution executes within the same scope that encloses the free variables it references, the compiler should detect that and not create a closure over the lambda, because it's not needed.

The way to verify that would be by testing it using something like this:

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();  
   }

}
于 2012-02-21T07:06:35.710 回答
0

我为您分析了您的基准并确定了许多事情:

首先,它把一半的时间花在了return this.GetDayData(day).ToList();电话上ToList。如果您删除它并手动迭代结果,您可以测量方法中的相对差异。

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

第三,“慢”方法在 x86 上确实很慢,但与 x64 上的“快”方法差不多快。我相信这是由于不同的 JITter 如何创建代表。如果从结果中扣除委托创建,“快”和“慢”方法的速度是相同的。

第四,如果您实际上多次调用迭代器(在我的计算机上,以 x64 为目标,使用RangeCount = 8),“慢”实际上比“foreach”快,而“快”比所有迭代器都快。

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

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

public class SuperFastLinqRangeLookup<TItem> : RangeLookupBase<TItem>
    where TItem : RangeItem
{

    public SuperFastLinqRangeLookup(DateTime start, DateTime end, IEnumerable<TItem> items)
        : base(start, end, items)
    {
        // create delegate only once
        predicate = i => i.IsDayWithinRange(day);
    }

    DateTime day;
    Func<TItem, bool> predicate;

    public override IEnumerable<TItem> GetDayData(DateTime day)
    {
        this.day = day; // set captured day to correct value
        return this.items.Where(predicate);
    }
}
于 2011-11-29T04:53:08.343 回答