“yield return”比“旧式”的“return”慢吗?

6

我正在进行关于 yield return 性能的测试,发现它比普通的 return 操作慢。

我测试了值类型变量(int、double 等)和一些引用类型(string 等),发现在两种情况下使用 yield return 都比较慢。那么为什么还要使用它呢?

看看我的例子:

public class YieldReturnTeste
{
    private static IEnumerable<string> YieldReturnTest(int limite)
    {
        for (int i = 0; i < limite; i++)
        {
            yield return i.ToString();
        }
    }

    private static IEnumerable<string> NormalReturnTest(int limite)
    {
        List<string> listaInteiros = new List<string>();

        for (int i = 0; i < limite; i++)
        {
            listaInteiros.Add(i.ToString());
        }
        return listaInteiros;
    }

    public static void executaTeste()
    {
        Stopwatch stopWatch = new Stopwatch();

        stopWatch.Start();

        List<string> minhaListaYield = YieldReturnTest(2000000).ToList();

        stopWatch.Stop();

        TimeSpan ts = stopWatch.Elapsed;


        string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}",

        ts.Hours, ts.Minutes, ts.Seconds,

        ts.Milliseconds / 10);

        Console.WriteLine("Yield return: {0}", elapsedTime);

        //****

        stopWatch = new Stopwatch();

        stopWatch.Start();

        List<string> minhaListaNormal = NormalReturnTest(2000000).ToList();

        stopWatch.Stop();

        ts = stopWatch.Elapsed;


        elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}",

        ts.Hours, ts.Minutes, ts.Seconds,

        ts.Milliseconds / 10);

        Console.WriteLine("Normal return: {0}", elapsedTime);
    }
}

2
注意内存消耗!List<string>方法消耗O(n)内存,而yield/Enumerator方法则具有O(1)的内存消耗。对于非常大的列表来说,这非常关键。而且你可以更容易地链接可枚举对象,无需额外的临时列表。这是一个更一般的讨论:https://dev59.com/5XA65IYBdhLWcg3wyh57 - Thomas B.
两个问题:首先,NormalReturnTest 应该将其列表长度预初始化为 limite。其次,我相当确定当在 List 上操作 .ToList() 方法时,它会对其基础数组进行特殊检查,并执行数组复制,而不是迭代列表并逐个复制项目,从而产生完全不同的结果。而你的 yield return 可枚举对象上的 .ToList() 将不得不迭代每个元素并构建数组(沿途导致多次调整大小以达到 2000000 个元素)。你正在测量错误的东西。 - Chris Sinclair
1
@WilnerAvila:小心不要完全消除迭代。你是把它们放在foreach循环中吗?因为如果你不迭代它,yield return实际上就什么也不会做。 - Chris Sinclair
@ChrisSinclair 是的,我现在已经做到了,yield 需要比普通方法少一半的时间。当我调用显式迭代时,yield return 只会执行一次吗? - Marcel James
@WilnerAvila 是的;它的执行被延迟,因此其中的代码只会在每个 yield return 调用时执行,仅当在迭代期间调用 MoveNext 方法时才会执行。 - Chris Sinclair
显示剩余4条评论
5个回答

15
考虑一下File.ReadAllLinesFile.ReadLines之间的区别。 ReadAllLines将所有行加载到内存中并返回一个string[]。如果文件很小,则完全没有问题。但是如果文件大于内存限制,就会耗尽内存。
另一方面,ReadLines使用yield return逐行返回。使用它,您可以读取任何大小的文件。它不会将整个文件加载到内存中。
假设您想要查找第一行包含单词“foo”的行,然后退出。使用ReadAllLines,即使“foo”出现在第一行,您也必须将整个文件读入内存中。使用ReadLines,您只需读取一行。哪种更快呢?
这并不是唯一的原因。考虑一个读取文件并处理每行的程序。使用File.ReadAllLines,您最终会得到:
string[] lines = File.ReadAllLines(filename);
for (int i = 0; i < lines.Length; ++i)
{
    // process line
}

程序执行所需的时间等于读取文件所需的时间加上处理行的时间。假设处理需要很长时间,您想使用多个线程加快速度。因此您可以进行以下操作:

lines = File.ReadAllLines(filename);
Parallel.Foreach(...);

但是读取是单线程的。你的多个线程无法启动,直到主线程加载完整个文件。

但是使用ReadLines,你可以做如下操作:

Parallel.Foreach(File.ReadLines(filename), line => { ProcessLine(line); });

它立即启动多个线程,这些线程在读取其他行的同时进行处理。因此,读取时间与处理时间重叠,这意味着您的程序将执行得更快。

我使用文件展示示例是因为这样更容易演示概念,但对于内存中的集合也同样适用。使用yield return将使用更少的内存,并且在调用只需要查看集合一部分的方法(例如Enumerable.AnyEnumerable.First等)时可能更快。


1
只是一个问题:如果方法“ReadLines”没有将所有文件放入内存,那么每读取一行,它是否执行磁盘访问? - Marcel James
2
@MichelAlmeida:不是的。底层流以4千字节或更多的块加载文件,然后从中解析行。因此,磁盘读取次数最小化。此外,操作系统通常启用某种类型的预读缓存,因此当流请求更多信息时,它已经在内存中,“读取”只是将数据从一个内存位置复制到另一个位置。 - Jim Mischel
好的,那么它仍然需要进行多个磁盘访问,但是通过“缓存”这一机制,它仍然比读取整个文件并将其全部放入内存要更好,对吗? 注意:该死,我无法引用你。 - Marcel James
1
@MichelAlmeida:即使将整个文件读入内存,也会进行多次读取。 对你来说可能看起来像一次读取,但在底层它正在读取一块数据,解析出行,读取另一块数据等等。 - Jim Mischel

2

首先,它是一个方便的功能。其次,它允许您进行“懒惰返回”,这意味着只有在获取值时才会对其进行评估。这在像数据库查询或者您不想完全迭代的集合中非常有价值。第三,它在某些场景下可能更快。第四,差异是什么?可能非常微小,所以这是一种微观优化。


1

0

.ToList()虽然必要,以便真正完成IEnumerable的延迟迭代,但会阻碍核心部分的测量。

至少初始化列表到已知大小很重要:

const int listSize=2000000; var tempList = new List(listSize);

...

列表 tempList = YieldReturnTest(listSize).ToList();

备注:在我的机器上,这两个调用花费的时间大致相同。没有区别(Mono 4 on repl.it)。


0

我使用yield return从算法中获取结果。每个结果都基于先前的结果,但我不需要全部结果。我使用foreach和yield return来检查每个结果,并在获得符合要求的结果时中断foreach循环。

这个算法相当复杂,因此我认为在每个yield return之间保存状态需要进行一些相当不错的工作。

我注意到它比传统的return慢3%-5%,但是由于不需要生成所有结果而获得的改进远远大于性能损失。


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