簡體   English   中英

悖論:為什么收益率的回報快於此處的列表

[英]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 + ToListList + Set initial capacity ),則yield 慢得多。

暫無
暫無

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

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