用IEnumerable / yield解释这个奇怪的行为

本文关键字:IEnumerable yield 解释 | 更新日期: 2023-09-27 18:15:40

猜一猜…当I == 0时,这个程序需要多长时间才能产生第一个输出?应该是即时的,对吧?通过对yield的惰性评估它应该在那之后快速连续地产生输出,对吧?

static void Main(string[] args)
{
   Stopwatch stopwatch = Stopwatch.StartNew();
   int i = 0;
   foreach (var item in massiveYieldStatement())
   {
        if (i++ % 10000 == 0) 
           Console.WriteLine(stopwatch.ElapsedMilliseconds / 1000);
   }
   Console.ReadKey();
}
static IEnumerable<string> massiveYieldStatement()
{
   yield return "a"; 
   yield return "a";
   .. repeat 200,000 times !!
   yield return "a";
}

但它没有!它会在4到21分钟之间没有输出,然后很快完成——在一个案例中不到60毫秒!在这几分钟内,一个核心的CPU价值的100%被使用,内存使用也在增长。在我遇到这种情况的实际场景中,甚至在第一次迭代发生之前就抛出了Stackoverflow异常!我在Visual Studio的调试模式和命令提示符的发布模式下尝试过。我已经在Windows 7 x64和Windows Server 2008 R2 x64上试过了。

谁能解释一下这里发生了什么?你觉得合适吗?

注意:这不是真正的代码:真正的代码有更少的yield语句,但更复杂。

用IEnumerable / yield解释这个奇怪的行为

此代码生成的程序集大小为几MB。yield return是一个特殊的野兽,因为它看起来看似简单,但c#编译器实际上生成一个类("状态机")来实现massiveYieldStatement方法。我很确定,您正在等待JIT编译器编译该类的MoveNext()方法(您可以用ildasm验证:如果您尝试打开MoveNext()方法,它也需要很多时间)。

问题不在于收益率,而在于返回200K收益率的函数(顺便说一下)。10万行已经拖慢了我的VS)。每次在从IEnumerable<string>.GetEnumerator返回的IEnumerator上执行第一个MoveNext()时,都需要对其进行评估并生成一个状态类new。

static IEnumerable<string> massiveYieldStatement()
{
    for(int i = 0; i < 200000; ++i)
        yield return "a";
}

运行速度如预期的那样快,因为求值很快。