[英]Paradox: Why is yield return faster than list here
人們已經證明了無數次, yield return
慢於list
。
然而,當我嘗試一個基准測試時,我得到了相反的結果:
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
是狀態機可憎的,但速度更快。 為什么?
編輯:所有答案都復制了,確實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.
然而,這些見解讓人心碎。 這意味着所有那些使用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;
}
因此,yield會自動內聯到數組中。 列表不是。
如果使用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 |
注意分配。 對於某些情況,這可能會有所不同。
我們可以通過分配正確的大小列表來抵消一些分配,但最終這不是蘋果對蘋果的比較。 下面的數字。
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 |
代碼如下。
[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;
}
}
您必須考慮以下幾點:
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;
}
}
結果(發布版本):
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
如果我們比較可比情況( Yield + ToList
& List + Set initial capacity
),則yield
要慢得多。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.