簡體   English   中英

C#收益率回報表現

[英]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通過這個會遍歷數字110的包容性。

現在讓我們按照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); 並完成它。

如果我們需要在GetList1GetList2實際執行某些GetList2 (例如轉換元素,過濾元素,跟蹤平均值等),那么GetList1將在內存上更快更GetList1 輕得多,如果我們永遠不會調用ToList()就可以了,稍微ligher如果我們調用ToList()因為再次,更快,更輕ToList()被抵消GetList2是由完全相同的量在首位慢和更重。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM