[英]C# yield return performance
使用yield return語法的方法后面的底層集合保留了多少空間當我在其上執行ToList()時? 如果與我創建具有預定義容量的列表的標准方法相比,它有可能重新分配並因此降低性能?
這兩種情況:
public IEnumerable<T> GetList1()
{
foreach( var item in collection )
yield return item.Property;
}
public IEnumerable<T> GetList2()
{
List<T> outputList = new List<T>( collection.Count() );
foreach( var item in collection )
outputList.Add( item.Property );
return outputList;
}
yield return
不會創建一個必須調整大小的數組,就像List
所做的那樣; 相反,它使用狀態機創建IEnumerable
。
例如,讓我們采用這種方法:
public static IEnumerable<int> Foo()
{
Console.WriteLine("Returning 1");
yield return 1;
Console.WriteLine("Returning 2");
yield return 2;
Console.WriteLine("Returning 3");
yield return 3;
}
現在讓我們調用它並將可枚舉賦值給變量:
var elems = Foo();
在代碼中沒有 Foo
尚未執行。 控制台上不會打印任何內容。 但是如果我們迭代它,就像這樣:
foreach(var elem in elems)
{
Console.WriteLine( "Got " + elem );
}
在foreach
循環的第一次迭代中,將執行Foo
方法,直到第一次yield return
。 然后,在第二次迭代中,該方法將從它停止的位置“恢復”(在yield return 1
),並執行直到下一次yield return
。 所有后續元素都相同。
在循環結束時,控制台將如下所示:
Returning 1
Got 1
Returning 2
Got 2
Returning 3
Got 3
這意味着您可以編寫如下方法:
public static IEnumerable<int> GetAnswers()
{
while( true )
{
yield return 42;
}
}
你可以調用GetAnswers
方法,每次你請求一個元素時,它都會給你42; 序列永遠不會結束。 您無法使用List
執行此操作,因為列表必須具有有限的大小。
使用yield return語法為方法后面的底層集合保留了多少空間?
沒有潛在的集合。
有一個對象,但它不是一個集合。 它將占用多少空間取決於它需要跟蹤的內容。
它有可能重新分配
沒有。
如果與我創建具有預定義容量的列表的標准方法相比,從而降低性能?
與創建具有預定義容量的列表相比,它幾乎肯定會占用更少的內存。
我們來試試一個手冊。 假設我們有以下代碼:
public static IEnumerable<int> CountToTen()
{
for(var i = 1; i != 11; ++i)
yield return i;
}
要foreach
通過這個會遍歷數字1
到10
的包容性。
現在讓我們按照yield
不存在的方式做到這一點。 我們做的事情如下:
private class CountToTenEnumerator : IEnumerator<int>
{
private int _current;
public int Current
{
get
{
if(_current == 0)
throw new InvalidOperationException();
return _current;
}
}
object IEnumerator.Current
{
get { return Current; }
}
public bool MoveNext()
{
if(_current == 10)
return false;
_current++;
return true;
}
public void Reset()
{
throw new NotSupportedException();
// We *could* just set _current back, but the object produced by
// yield won't do that, so we'll match that.
}
public void Dispose()
{
}
}
private class CountToTenEnumerable : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator()
{
return new CountToTenEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public static IEnumerable<int> CountToTen()
{
return new CountToTenEnumerable();
}
現在,由於各種原因,這與使用yield
可能從版本中獲得的代碼完全不同,但基本原理是相同的。 正如您所看到的,對象涉及兩個分配(相同的數字就好像我們有一個集合,然后foreach
做了一個foreach
)和一個int的存儲。 在實踐中,我們可以期望yield
存儲比這更多的字節,但不是很多。
編輯: yield
實際上是一個技巧,在獲得該對象的同一線程上的第一個GetEnumerator()
調用返回同一個對象,為兩種情況執行雙重服務。 由於這涵蓋了超過99%的用例,因此yield
實際上只進行了一次分配而不是兩次。
現在讓我們來看看:
public IEnumerable<T> GetList1()
{
foreach( var item in collection )
yield return item.Property;
}
雖然這會導致使用更多內存而不僅僅是return collection
,但它不會導致更多內容; 枚舉器生成的唯一真正需要跟蹤的是通過在collection
上調用GetEnumerator()
然后包裝它而生成的枚舉器。
與你提到的浪費的第二種方法相比,這將大大減少內存,並且要快得多。
編輯:
你已經改變了你的問題,包括“我在其上執行ToList()時的語法”,值得考慮。
現在,我們需要增加第三種可能性:了解集合的大小。
在這里,有可能使用new List(capacity)
將阻止正在構建的列表的分配。 這確實可以節省很多。
如果在其上調用ToList
的對象實現了ICollection<T>
那么ToList
將首先完成對T
的內部數組的單個分配,然后調用ICollection<T>.CopyTo()
。
這意味着您的GetList2
將導致比GetList1
更快的ToList()
。
但是,你的GetList2
已經浪費了時間和內存來做ToList()
無論如何都會對GetList1
的結果做什么!
它應該在這里做的只是return new List<T>(collection);
並完成它。
如果我們需要在GetList1
或GetList2
實際執行某些GetList2
(例如轉換元素,過濾元素,跟蹤平均值等),那么GetList1
將在內存上更快更GetList1
。 輕得多,如果我們永遠不會調用ToList()
就可以了,稍微ligher如果我們調用ToList()
因為再次,更快,更輕ToList()
被抵消GetList2
是由完全相同的量在首位慢和更重。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.