使用IEnumerable/yield时出现奇怪的行为,请解释一下。

4

你猜,在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分钟,然后快速完成 - 在一个案例中低于60ms!在这些分钟中,使用了一个核心的CPU价值100%,并且内存使用量增加。在我遇到这个问题的实际场景中,甚至在第一次迭代之前就会抛出Stackoverflow异常!我已经尝试在Visual Studio的调试模式和从命令提示符中的发行模式中进行了测试。我已经在Windows 7 x64和Windows Server 2008 R2 x64上进行了尝试。
有人能解释这里到底发生了什么吗?你可以重现这个问题吗?
注意:这不是真正的代码:真正的代码yield语句要少得多,但复杂得多。这只是最简单的重现。

3
200000个'yield'语句?我很高兴C#团队没有浪费时间去优化它...我从来没有深入研究过'yield'是如何实现的,但我猜你的答案就在那里。你所展示的代码离你所谓的“实际场景”有多接近? - Kobi
4
如果您知道它在60毫秒内完成,那么似乎暂停发生在任何代码执行之前,这让我认为JIT编译器存在问题。很可能正在尝试优化一个拥有200,000个switch语句的情况,导致一些病态行为。您是否有必要使用如此多yield语句的迭代器呢? - Gabe
1
执行两次foreach。如果第二次非常快,那么很可能是JIT编译的开销导致了你的观察结果。 - Ani
1
你试过Ani建议的吗?如果是这样,它将证明是JIT“问题”... - Ventsyslav Raikov
现在正在尝试Ani的建议,需要几分钟时间... - Ian Mercer
显示剩余2条评论
2个回答

4
这段代码生成的汇编代码有几MB大小。`yield return`很特殊,看上去非常简单,但实际上C#编译器会生成一个类(“状态机”)来实现 massiveYieldStatement 方法。我相信你正在等待JIT编译器编译该类的MoveNext()方法(你可以通过ildasm验证:如果尝试打开MoveNext()方法,同样需要很长时间)。

2
问题不在于产量,而在于返回200K个产量的函数(顺便说一句,100K行已经让我的VS变慢了)。每次您在从IEnumerable.GetEnumerator返回的IEnumerator上执行第一个MoveNext()时,都需要评估它并生成一个新的状态类。请注意保留HTML标记。
static IEnumerable<string> massiveYieldStatement()
{
    for(int i = 0; i < 200000; ++i)
        yield return "a";
}

由于评估速度快,因此运行速度如预期快。


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