悖论:为什么收益率回报比这里列出的更快

人们已经无数次证明,那yield returnlist.

但是,当我尝试基准测试时,得到了相反的结果:


Results:

TestYield: Time =1.19 sec

TestList : Time =4.22 sec

在这里,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;

}

以下是我使用它们的方式:


foreach (var value in CreateNumbers()) sum += value;

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


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


编辑:所有答案都被复制,确实产量比列表快。


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.

然而,这些见解令人心碎。这意味着所有那些从 1960 年起使用 list 作为默认集合的程序员都是错误的,应该被解雇(解雇),因为他们没有使用最好的工具来解决这种情况(yield)。


答案认为,收益率应该更快,因为它没有实现。


1)我不接受这个逻辑。Yield 背后有内部逻辑,它不是“理论模型”,而是编译器构造。因此,它会在消费时自动实现。我不接受它“没有实现”的论点,因为成本已经在使用时支付。


2)如果船可以走海路,而老妇人不行,你不能要求船“走陆路”。正如你在这里对列表所做的那样。如果一个列表需要具体化,而 yield 不需要,那不是“产量问题”,而是一个“特性”。不应仅仅因为它有更多用途而在测试中惩罚产量。


3)我在这里争论的是,如果您知道整个 SET将被消耗,那么测试的目的是找到“最快的集合”来消耗/返回方法返回的结果。


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;

}

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


慕尼黑8549860
浏览 107回答 2
2回答

米琪卡哇伊

如果您使用产量测量版本而不具体化列表,它将比其他版本具有优势,因为它不必分配和调整大列表的大小(以及触发 GC)。根据您的编辑,我想添加以下内容:但是,请记住,从语义上讲,您正在查看两种不同的方法。一个产生一个集合。它的大小是有限的,您可以存储对集合的引用、更改其元素并共享它。另一个产生一个序列。它可能是无限的,每次迭代它都会得到一个新副本,并且它后面可能有也可能没有集合。它们不是同一件事。编译器不会创建集合来实现序列。如果您通过在幕后实现集合来实现序列,您将看到与使用列表的版本相似的性能。BenchmarkDotNet 默认情况下不允许您对延迟执行进行计时,因此您必须构建一个使用我在下面所做的方法的测试。我通过 BenchmarkDotNet 运行它并得到以下结果。&nbsp; &nbsp; &nbsp; &nbsp;Method |&nbsp; &nbsp; &nbsp;Mean |&nbsp; &nbsp; Error |&nbsp; &nbsp;StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |------------- |---------:|---------:|---------:|------------:|------------:|------------:|--------------------:|&nbsp;ConsumeYield | 475.5 us | 7.010 us | 6.214 us |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;- |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;- |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;- |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 40 B |&nbsp; ConsumeList | 958.9 us | 7.271 us | 6.801 us |&nbsp; &nbsp; 285.1563 |&nbsp; &nbsp; 285.1563 |&nbsp; &nbsp; 285.1563 |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;1049024 B |注意分配。在某些情况下,这可能会有所不同。我们可以通过分配正确的大小列表来抵消一些分配,但最终这不是苹果对苹果的比较。下面的数字。&nbsp; &nbsp; &nbsp; &nbsp;Method |&nbsp; &nbsp; &nbsp;Mean |&nbsp; &nbsp; &nbsp;Error |&nbsp; &nbsp; StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |------------- |---------:|----------:|----------:|------------:|------------:|------------:|--------------------:|&nbsp;ConsumeYield | 470.8 us |&nbsp; 2.508 us |&nbsp; 2.346 us |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;- |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;- |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;- |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 40 B |&nbsp; ConsumeList | 836.2 us | 13.456 us | 12.587 us |&nbsp; &nbsp; 124.0234 |&nbsp; &nbsp; 124.0234 |&nbsp; &nbsp; 124.0234 |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 400104 B |代码如下。[MemoryDiagnoser]public class Test{&nbsp; &nbsp; static void Main(string[] args)&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; var summary = BenchmarkRunner.Run<Test>();&nbsp; &nbsp; }&nbsp; &nbsp; public int Size = 100000;&nbsp; &nbsp; [Benchmark]&nbsp; &nbsp; public int ConsumeYield()&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; var sum = 0;&nbsp; &nbsp; &nbsp; &nbsp; foreach (var x in CreateNumbersYield()) sum += x;&nbsp; &nbsp; &nbsp; &nbsp; return sum;&nbsp; &nbsp; }&nbsp; &nbsp; [Benchmark]&nbsp; &nbsp; public int ConsumeList()&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; var sum = 0;&nbsp; &nbsp; &nbsp; &nbsp; foreach (var x in CreateNumbersList()) sum += x;&nbsp; &nbsp; &nbsp; &nbsp; return sum;&nbsp; &nbsp; }&nbsp; &nbsp; public IEnumerable<int> CreateNumbersYield() //for yield&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; for (int i = 0; i < Size; i++) yield return i;&nbsp; &nbsp; }&nbsp; &nbsp; public IEnumerable<int> CreateNumbersList() //for list&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; var list = new List<int>();&nbsp; &nbsp; &nbsp; &nbsp; for (int i = 0; i < Size; i++) list.Add(i);&nbsp; &nbsp; &nbsp; &nbsp; return list;&nbsp; &nbsp; }}

HUH函数

您必须考虑以下几点:List<T>消耗内存,但您可以一次又一次地迭代它而无需任何额外资源。为了达到同样的效果yield,您需要通过 实现序列ToList()。生产时最好设置容量List<T>。这将避免内部数组调整大小。这是我所拥有的:class Program{&nbsp; &nbsp; static void Main(string[] args)&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; // warming up&nbsp; &nbsp; &nbsp; &nbsp; CreateNumbersYield(1);&nbsp; &nbsp; &nbsp; &nbsp; CreateNumbersList(1, true);&nbsp; &nbsp; &nbsp; &nbsp; Measure(null, () => { });&nbsp; &nbsp; &nbsp; &nbsp; // testing&nbsp; &nbsp; &nbsp; &nbsp; var size = 1000000;&nbsp; &nbsp; &nbsp; &nbsp; Measure("Yield", () => CreateNumbersYield(size));&nbsp; &nbsp; &nbsp; &nbsp; Measure("Yield + ToList", () => CreateNumbersYield(size).ToList());&nbsp; &nbsp; &nbsp; &nbsp; Measure("List", () => CreateNumbersList(size, false));&nbsp; &nbsp; &nbsp; &nbsp; Measure("List + Set initial capacity", () => CreateNumbersList(size, true));&nbsp; &nbsp; &nbsp; &nbsp; Console.ReadLine();&nbsp; &nbsp; }&nbsp; &nbsp; static void Measure(string testName, Action action)&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; var sw = new Stopwatch();&nbsp; &nbsp; &nbsp; &nbsp; sw.Start();&nbsp; &nbsp; &nbsp; &nbsp; action();&nbsp; &nbsp; &nbsp; &nbsp; sw.Stop();&nbsp; &nbsp; &nbsp; &nbsp; Console.WriteLine($"{testName} completed in {sw.Elapsed}");&nbsp; &nbsp; }&nbsp; &nbsp; static IEnumerable<int> CreateNumbersYield(int size) //for yield&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; for (int i = 0; i < size; i++)&nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; yield return i;&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }&nbsp; &nbsp; static IEnumerable<int> CreateNumbersList(int size, bool setInitialCapacity) //for list&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; var list = setInitialCapacity ? new List<int>(size) : new List<int>();&nbsp; &nbsp; &nbsp; &nbsp; for (int i = 0; i < size; i++)&nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; list.Add(i);&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; return list;&nbsp; &nbsp; }}结果(发布版本):Yield completed in 00:00:00.0001683Yield + ToList completed in 00:00:00.0121015List completed in 00:00:00.0060071List + Set initial capacity completed in 00:00:00.0033668如果我们比较可比较的情况(Yield + ToList& List + Set initial capacity),yield速度要慢得多。
打开App,查看更多内容
随时随地看视频慕课网APP