悖论:为什么收益率快于此处的列表

时间:2018-12-18 06:54:53

标签: c# performance benchmarking

人们已经证明了无数次,yield returnlist慢。

示例:Is 'yield return' slower than "old school" return?

但是,当我尝试使用基准测试时,结果却相反:

Results:
TestYield: Time =1.19 sec
TestList : Time =4.22 sec

在此,列表的速度降低了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年及以后使用列表作为默认集合的程序员都是错误的,应该将其开枪(开除),因为他们没有使用针对情况的最佳工具(收益)。

答案认为,产量应该更快,因为它尚未实现。

1)我不接受这种逻辑。 Yield在幕后具有内部逻辑,它不是“理论模型”,而是编译器构造。因此,它会自动实现消耗。我不接受它“没有实现”的说法,因为费用已经由USE支付。

2)如果小船可以海上航行,但是老妇人不能,则不能要求小船“陆上航行”。就像您在此处处理列表一样。如果列表要求实现,而收益不需要,那不是“收益问题”,而是“功能”。收益率不应仅仅因为它有更多用途而在测试中受到惩罚。

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将自动内联到数组中。列表不是。

2 个答案:

答案 0 :(得分:8)

如果在不具体化列表的情况下使用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;
    }
}

答案 1 :(得分:7)

您必须考虑以下几点:

  • 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的速度要慢得多。