Люди бесчисленное количество раз доказывали, что 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 автоматически встраивается в массив. Списка нет.
Ага, как вы это сравнивали?
List<int> уже материализуется, подход yield - нет. Для сравнения результатов добавьте var result = CreateNumbers().ToList(); при подходе доходности
В моем ориентир я обнаружил, что (для размера 100 000 с 10 000 итераций) метод yield занял 14 815 мс, а метод списка - 8 433 мс.
Я хотел бы увидеть базовый код: D
«доходность возвращается медленнее, чем указано в списке». Вы не можете сравнивать производительность оператора с объектом. То, что вы действительно сравниваете, - это реализация конечного автомата с несколькими перераспределениями массива, который также копирует данные во вновь выделенный массив. В зависимости от настроек теста он может быть быстрее или медленнее.
Если вы ожидаете, что итератор здесь будет медленнее, то какой смысл использовать это как языковую функцию для начала? Это именно та ситуация, в которой он разработан, чтобы быть наиболее эффективным. Если бы это было бесполезно в данной ситуации, практически не было бы ситуаций, в которых он был бы полезен.





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