Парадокс: почему доходность возвращается быстрее, чем указано здесь

Люди бесчисленное количество раз доказывали, что 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 - это мерзость конечного автомата, но она быстрее. Почему?

Обновлено: все ответы воспроизводятся, что действительно доходность быстрее, чем список.

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 есть внутренняя логика, это не «теоретическая модель», а конструкция компилятора. Следовательно, он автоматически материализуется при потреблении. Я не принимаю аргумент о том, что это «не материализовалось», поскольку стоимость ЕГЭ уже оплачена.

2) Если лодка может путешествовать по морю, а старуха - нет, вы не можете требовать, чтобы лодка «двигалась по суше». Как вы сделали здесь со списком. Если список требует материализации, а yield - нет, это не «проблема 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 автоматически встраивается в массив. Списка нет.

Показать код теста

FCin 18.12.2018 07:56

Ага, как вы это сравнивали?

Llama 18.12.2018 07:57

List<int> уже материализуется, подход yield - нет. Для сравнения результатов добавьте var result = CreateNumbers().ToList(); при подходе доходности

fubo 18.12.2018 07:58

В моем ориентир я обнаружил, что (для размера 100 000 с 10 000 итераций) метод yield занял 14 815 мс, а метод списка - 8 433 мс.

Llama 18.12.2018 08:10

Я хотел бы увидеть базовый код: D

Hasan Emrah Süngü 18.12.2018 08:13

«доходность возвращается медленнее, чем указано в списке». Вы не можете сравнивать производительность оператора с объектом. То, что вы действительно сравниваете, - это реализация конечного автомата с несколькими перераспределениями массива, который также копирует данные во вновь выделенный массив. В зависимости от настроек теста он может быть быстрее или медленнее.

Zdeslav Vojkovic 18.12.2018 08:13

Если вы ожидаете, что итератор здесь будет медленнее, то какой смысл использовать это как языковую функцию для начала? Это именно та ситуация, в которой он разработан, чтобы быть наиболее эффективным. Если бы это было бесполезно в данной ситуации, практически не было бы ситуаций, в которых он был бы полезен.

Servy 18.12.2018 20:05
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
7
317
2

Ответы 2

Если вы измеряете версию с помощью yield без материализации списка, она будет иметь преимущество перед другой версией, поскольку ей не придется выделять и изменять размер большого списка (а также запускать сборщик мусора).

На основании вашего редактирования я хотел бы добавить следующее:

However, keep in mind that semantically you're looking at two different methods. One produces a collection. It is finite in size, you can store references to the collection, change its elements, and share it.

The other produces a sequence. It is potentially unbounded, you get a new copy each time you iterate over it, and there may or may not be a collection behind it.

They are not the same thing. The compiler doesn't create a collection to implement a sequence. If you implement a sequence by materializing a collection behind the scenes you will see similar performance as the version that uses a list.

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;
    }
}

Что если вы инициализируете список размером?

Magnus 18.12.2018 08:22

@Magnus, это улучшит ситуацию, так как у вас будет меньше выделений (я обновил ответ). Тем не менее, я считаю, что сборка мусора здесь - различие, и поскольку мы сравниваем один подход, который материализует список, с другим, это не совсем сравнение яблок с яблоками.

Brian Rasmussen 18.12.2018 08:28

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

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

Другие вопросы по теме