悖论:为什么收益率的回报快于此处的列表

use*_*597 2 c# performance benchmarking

人们已经证明无数次,这yield return比人们慢list.

示例:"收益率回报"是否慢于"旧学校"回归?

然而,当我尝试一个基准测试时,我得到了相反的结果:

Results:
TestYield: Time =1.19 sec
TestList : Time =4.22 sec
Run Code Online (Sandbox Code Playgroud)

在这里,List慢了400%.无论大小如何都会发生 这毫无意义.

IEnumerable<int> CreateNumbers() //for yield
{
    for (int i = 0; i < Size; i++) yield return i;
}

IEnumerable<int> CreateNumbers() //for list
{
    var list = new List<int>();
    for (int i = 0; i < Size; i++) list.Add(i);
    return list;
}
Run Code Online (Sandbox Code Playgroud)

以下是我如何使用它们:

foreach (var value in CreateNumbers()) sum += value;
Run Code Online (Sandbox Code Playgroud)

我使用所有正确的基准规则来避免冲突的结果,所以这不是问题.

如果你看到底层代码,yield return是一个状态机憎恶,但它更快.为什么?

编辑:所有答案都复制了,确实Yield比列表更快.

New Results With Size set on constructor:
TestYield: Time =1.001
TestList: Time =1.403
From a 400% slower difference, down to 40% slower difference.
Run Code Online (Sandbox Code Playgroud)

然而,这些见解让人心碎.这意味着所有那些使用list作为默认集合的1960年及以后的程序员都是错误的并且应该被拍摄(触发),因为他们没有使用最好的工具来处理这种情况(产量).

答案认为产量应该更快,因为它没有实现.

1)我不接受这种逻辑.Yield具有幕后的内部逻辑,它不是"理论模型",而是编译器构造.因此它会自动实现消费.我不接受它"没有实现"的论点,因为已经支付了USE的费用.

2)如果一艘船可以在海上旅行,但是一位老妇人不能,则不能要求船"陆上移动".正如你在这里列出的那样.如果列表需要实现,而yield不需要,那么这不是"产量问题",而是"特征".产量不应该在测试中受到惩罚,因为它有更多的用途.

3)我在这里争论的是,测试的目的是找到消耗/返回方法返回的结果的"最快集合",如果你知道将使用整个集合.

yield是否成为从方法返回列表参数的新"事实上的标准".

Edit2:如果我使用纯内联数组,它会获得与Yield相同的性能.

Test 3:
TestYield: Time =0.987
TestArray: Time =0.962
TestList: Time =1.516

int[] CreateNumbers()
{
    var list = new int[Size];
    for (int i = 0; i < Size; i++) list[i] = i;
    return list;
}
Run Code Online (Sandbox Code Playgroud)

因此,yield会自动内联到数组中.列表不是.

Bri*_*sen 8

如果使用yield测量版本而不实现列表,则它将优于其他版本,因为它不必分配和调整大型列表(以及触发GC).

根据您的编辑,我想添加以下内容:

但是,请记住,从语义上来说,您正在研究两种不同的方法.一个产生一个集合.它的大小有限,您可以存储对集合的引用,更改其元素并共享它.

另一个产生序列.它可能是无限的,每次迭代它时都会获得一个新副本,并且它背后可能有也可能没有集合.

它们不是同一件事.编译器不会创建集合来实现序列.如果通过物化幕后集合执行顺序,你会看到性能,使用列表中的版本类似.

BenchmarkDotNet不允许您默认延迟执行,因此您必须构建一个使用我在下面所做的方法的测试.我通过BenchmarkDotNet运行了这个并得到了以下内容.

       Method |     Mean |    Error |   StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
------------- |---------:|---------:|---------:|------------:|------------:|------------:|--------------------:|
 ConsumeYield | 475.5 us | 7.010 us | 6.214 us |           - |           - |           - |                40 B |
  ConsumeList | 958.9 us | 7.271 us | 6.801 us |    285.1563 |    285.1563 |    285.1563 |           1049024 B |
Run Code Online (Sandbox Code Playgroud)

注意分配.对于某些情况,这可能会有所不同.

我们可以通过分配正确的大小列表来抵消一些分配,但最终这不是苹果对苹果的比较.下面的数字.

       Method |     Mean |     Error |    StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
------------- |---------:|----------:|----------:|------------:|------------:|------------:|--------------------:|
 ConsumeYield | 470.8 us |  2.508 us |  2.346 us |           - |           - |           - |                40 B |
  ConsumeList | 836.2 us | 13.456 us | 12.587 us |    124.0234 |    124.0234 |    124.0234 |            400104 B |
Run Code Online (Sandbox Code Playgroud)

代码如下.

[MemoryDiagnoser]
public class Test
{
    static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<Test>();
    }

    public int Size = 100000;

    [Benchmark]
    public int ConsumeYield()
    {
        var sum = 0;
        foreach (var x in CreateNumbersYield()) sum += x;
        return sum;
    }

    [Benchmark]
    public int ConsumeList()
    {
        var sum = 0;
        foreach (var x in CreateNumbersList()) sum += x;
        return sum;
    }

    public IEnumerable<int> CreateNumbersYield() //for yield
    {
        for (int i = 0; i < Size; i++) yield return i;
    }

    public IEnumerable<int> CreateNumbersList() //for list
    {
        var list = new List<int>();
        for (int i = 0; i < Size; i++) list.Add(i);
        return list;
    }
}
Run Code Online (Sandbox Code Playgroud)


Den*_*nis 7

您必须考虑以下几点:

  • List<T>消耗内存,但您可以一次又一次地迭代它而无需任何额外资源.要实现相同目的yield,您需要通过实现序列ToList().
  • 在生产时设定容量是可取的List<T>.这将避免内部数组调整大小.

这是我得到的:

class Program
{
    static void Main(string[] args)
    {
        // warming up
        CreateNumbersYield(1);
        CreateNumbersList(1, true);
        Measure(null, () => { });

        // testing
        var size = 1000000;

        Measure("Yield", () => CreateNumbersYield(size));
        Measure("Yield + ToList", () => CreateNumbersYield(size).ToList());
        Measure("List", () => CreateNumbersList(size, false));
        Measure("List + Set initial capacity", () => CreateNumbersList(size, true));

        Console.ReadLine();
    }

    static void Measure(string testName, Action action)
    {
        var sw = new Stopwatch();

        sw.Start();
        action();
        sw.Stop();

        Console.WriteLine($"{testName} completed in {sw.Elapsed}");
    }

    static IEnumerable<int> CreateNumbersYield(int size) //for yield
    {
        for (int i = 0; i < size; i++)
        {
            yield return i;
        }
    }

    static IEnumerable<int> CreateNumbersList(int size, bool setInitialCapacity) //for list
    {
        var list = setInitialCapacity ? new List<int>(size) : new List<int>();

        for (int i = 0; i < size; i++)
        {
            list.Add(i);
        }

        return list;
    }
}
Run Code Online (Sandbox Code Playgroud)

结果(发布版本):

Yield completed in 00:00:00.0001683
Yield + ToList completed in 00:00:00.0121015
List completed in 00:00:00.0060071
List + Set initial capacity completed in 00:00:00.0033668
Run Code Online (Sandbox Code Playgroud)

如果我们比较可比的情况下(Yield + ToList&List + Set initial capacity),yield很多慢.