修改一个含有"return"和"yield return"的方法

6

我知道在同一个方法中使用returnyield return是不可能的。

这是我想要优化的代码:

public IEnumerable<TItem> GetItems(int data)
{
    if (this.isSingleSet)
    {
        return this.singleSet; // is IEnumerable per-se
    }
    else
    {
        int index = this.GetSet(data);
        foreach(TKey key in this.keySets[index])
        {
            yield return this.items[key];
        }
    }
}

重要提示:我知道这段代码无法编译。这是我需要优化的代码。

我知道两种方法可以让这个方法工作:

  1. convert yield return part:

    ...
    else
    {
        int index = this.GetSet(data);
        return this.keySets[index].Select(key => this.items[key]);
    }
    
  2. convert return part:

    if (this.isSingleSet)
    {
        foreach(TItem item in this.singleSet)
        {
            yield return item;
        }
    }
    else ...
    
但是这两者之间有很大的速度差异。仅使用return语句(换句话说,使用Select())要慢得多(大约慢6倍)比yield return转换。
问题
您是否还有其他想法如何编写此方法?您是否有任何其他有价值的性能差异信息建议?
附加信息
我通过在for循环周围使用秒表来测量两种方法的速度。
Stopwatch s = new Stopwatch();
s.Start();
for(int i = 0; i < 1000000; i++)
{
    GetItems(GetRandomData()).ToList();
}
s.Stop();
Console.WriteLine(s.ElapsedMilliseconds);

每个循环都在单独的进程中运行,因此垃圾回收或其他任何因素都不会影响性能。

  1. 我已经使用一个方法版本运行了该程序
  2. 关闭它
  3. 重新编写该方法并再次运行它。

多次执行上述步骤以查看可靠的性能差异...


我的投票是#1。你有进行实际测试来确定它是否更慢吗? - Daniel A. White
1
你能否发布一下你用来评估这两种方法相对速度的代码片段? - Sergey Kalinichenko
你确定 yield return 更快吗?如果你进行了测量,请记得枚举返回的可枚举对象 - 如果不这样做,由于延迟执行,你将获得较小的执行时间。(换句话说,该怎么做可能取决于你是否总是需要返回的所有项,或者在某些情况下只需要从返回集合的开头获取一些项)。 - driis
是的。使用相同的测试数据执行这两种方法一百万次,当我使用“foreach”语句时,性能提升了6倍。 - Robert Koritnik
@driis:我稍微编辑了一下我的问题,添加了一些更多的信息。是的,我对两者进行了性能测试,正如您所看到的提供的代码一样,我通过调用ToList()来迭代结果可枚举对象。 - Robert Koritnik
2个回答

15

使用两个函数。外部函数由客户调用,执行所有非惰性部分(例如参数验证),您不希望延迟这些部分。私有worker函数执行惰性部分:

public IEnumerable<TItem> GetItems(int data) {
  if (this.isSingleSet) {
    return this.singleSet; // is IEnumerable per-se
  } else {
    return DoGetItems(data);
  }
}

private IEnumerable<TItem> DoGetItems(int data) {
  int index = this.GetSet(data);
  foreach(TKey key in this.keySets[index]) {
    yield return this.items[key];
  }
}

好主意!我不知道为什么我没想到这个! - Robert Koritnik
@RobertKoritnik 这是一个显而易见的方法 - 一旦你看到它(我现在无法回忆起何时看到它,但它导致了大量重写,以允许在结果为惰性时急切地完成帮助程序中的参数验证)。 - Richard
1
当然是这样的。我显然想太多关于复杂解决方案,没有看到最明显的一个... 提取一个方法 - Robert Koritnik

5
Select 的实现如下(去掉了错误检查):
public static IEnumerable<R> Select<A, R>(
    this IEnumerable<A> sequence, 
    Func<A, R> projection)
{
    foreach(A item in sequence) 
        yield return projection(item);
}

所以我很难相信你使用Select会比你已经有的几乎相同的foreach循环慢得多。它将通过进行错误检查(一次)和创建委托(一次)以及间接通过委托的轻微开销而变慢。但是,循环机制应该是相同的。
然而,如果我在性能分析中学到了一件事,那就是我的期望通常是完全错误的。你的分析运行结果表明你的应用程序瓶颈在哪里?让我们从事实而非猜测来推理。热点是什么?

Eric,你错过了主要的问题。问题在于投影,因为编译后的代码显示GetItems方法在每次调用时都会实例化一个新的Func<T1, T2>。当使用带有lambda的Select时,这种实例化是主要的性能杀手。或者看起来是这样。因为手动的foreach显然没有使用类似的东西。 - Robert Koritnik
只是为了明确:我的性能测量严格是在这个方法调用上完成的。我将在生产中多次调用它,因此我想测试两个版本的速度差异。 - Robert Koritnik
@RobertKoritnik: 你确定委托已被重新分配了吗? 今天我不在办公室,因此无法检查编译代码,但该lambda仅关闭“this”;我们应该建立每个对象缓存,以确保委托仅在每个实例中分配一次,然后每次重复使用。 另外,我会感到惊讶,分配委托的成本(一次)与调用ToList的成本相比很大。内存分配器与ToList相比极快。 - Eric Lippert
1
似乎是这样...因为当我查看反编译代码(在这里使用Reflector)时,我可以看到手动循环只使用编译器生成的枚举器/迭代器,但使用lambda会创建Func<>...最近我一直在使用lambda。很多次,所以每次我编写使用lambda的性能关键代码时,都会测试每个调用,以确保我不会降低性能。它们是如此不可预测...即使这个不使用自由变量的也似乎很慢。 - Robert Koritnik

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