Парадокс: почему доходность возвращается быстрее, чем список здесь - PullRequest
0 голосов
/ 18 декабря 2018

Люди бесчисленное количество раз доказывали, что yield return медленнее, чем list.

Пример: Является ли "доходность" медленнее, чем доходность "старой школы"?

Однако, когда я попытался выполнить тест, я получил противоположные результаты:

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 года и позже, которые использовали список в качестве коллекции по умолчанию, были неправы и должны были быть застрелены (уволены), потому что они не использовали лучший инструмент для ситуации (yield).

Ответы утверждали, что доходность должна быть быстрее, потому что она не материализована.

1) Я не принимаю эту логику.У выхода есть внутренняя логика, это не «теоретическая модель», а конструкция компилятора.Поэтому он автоматически материализуется на потребление.Я не принимаю аргумент, что это «не материализовалось», так как стоимость уже оплачена при использовании.

2) Если лодка может путешествовать по морю, но старуха не может, вы не можете,требовать, чтобы лодка "двигалась по суше"Как вы сделали здесь со списком.Если список требует материализации, а доходность - нет, то это не «проблема доходности», а «особенность».Доходность не должна быть оштрафована в тесте, просто потому, что он имеет больше применений.

3) Я утверждаю, что целью теста было найти «Самую быструю коллекцию» для использования / возврата возвращаемых результатов.с помощью метода, если вы знаете, что будет использоваться ENTIRE SET .

Станет ли 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 голосов
/ 18 декабря 2018

Пара вещей, которые вы должны принять во внимание:

  • 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 медленнее .

0 голосов
/ 18 декабря 2018

Если вы измеряете версию с помощью 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;
    }
}
...