"Возвращение урожая" медленнее, чем возвращение "старой школы"?
Я делаю несколько тестов о возврате производительности возврата, и я обнаружил, что он медленнее, чем нормальный возврат.
Я тестировал переменные значения (int, double и т.д.) и некоторые типы ссылок (строка и т.д.)... И доходность возврата в обоих случаях была медленнее. Зачем использовать его?
Посмотрите мой пример:
public class YieldReturnTeste
{
private static IEnumerable<string> YieldReturnTest(int limite)
{
for (int i = 0; i < limite; i++)
{
yield return i.ToString();
}
}
private static IEnumerable<string> NormalReturnTest(int limite)
{
List<string> listaInteiros = new List<string>();
for (int i = 0; i < limite; i++)
{
listaInteiros.Add(i.ToString());
}
return listaInteiros;
}
public static void executaTeste()
{
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
List<string> minhaListaYield = YieldReturnTest(2000000).ToList();
stopWatch.Stop();
TimeSpan ts = stopWatch.Elapsed;
string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}",
ts.Hours, ts.Minutes, ts.Seconds,
ts.Milliseconds / 10);
Console.WriteLine("Yield return: {0}", elapsedTime);
//****
stopWatch = new Stopwatch();
stopWatch.Start();
List<string> minhaListaNormal = NormalReturnTest(2000000).ToList();
stopWatch.Stop();
ts = stopWatch.Elapsed;
elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}",
ts.Hours, ts.Minutes, ts.Seconds,
ts.Milliseconds / 10);
Console.WriteLine("Normal return: {0}", elapsedTime);
}
}
Ответы
Ответ 1
Рассмотрим разницу между File.ReadAllLines
и File.ReadLines
.
ReadAllLines
загружает все строки в память и возвращает string[]
. Все хорошо и хорошо, если файл небольшой. Если файл больше, чем поместится в памяти, у вас закончится нехватка памяти.
ReadLines
, с другой стороны, использует yield return
для возврата по одной строке за раз. С его помощью вы можете прочитать любой файл размера. Он не загружает весь файл в память.
Скажем, вы хотели найти первую строку, содержащую слово "foo", а затем выйти. Используя ReadAllLines
, вам нужно будет прочитать весь файл в памяти, даже если "foo" встречается в первой строке. С помощью ReadLines
вы читаете только одну строку. Какой из них будет быстрее?
Это не единственная причина. Рассмотрим программу, которая читает файл и обрабатывает каждую строку. Используя File.ReadAllLines
, вы получите:
string[] lines = File.ReadAllLines(filename);
for (int i = 0; i < lines.Length; ++i)
{
// process line
}
Время, затрачиваемое на выполнение этой программы, равно времени, затрачиваемому на чтение файла, плюс время на обработку строк. Представьте, что обработка занимает так много времени, что вы хотите ускорить ее с помощью нескольких потоков. Итак, вы делаете что-то вроде:
lines = File.ReadAllLines(filename);
Parallel.Foreach(...);
Но чтение однопоточное. Ваши несколько потоков не могут запускаться до тех пор, пока основной поток не загрузит весь файл.
С ReadLines
, однако, вы можете сделать что-то вроде:
Parallel.Foreach(File.ReadLines(filename), line => { ProcessLine(line); });
Это немедленно запускает несколько потоков, которые обрабатываются одновременно с чтением других строк. Таким образом, время чтения перекрывается с временем обработки, что означает, что ваша программа будет выполняться быстрее.
Я показываю свои примеры, используя файлы, потому что это проще демонстрировать концепции таким образом, но то же самое верно для коллекций в памяти. Использование yield return
будет использовать меньше памяти и потенциально быстрее, особенно при вызове методов, которые должны смотреть только на часть коллекции (Enumerable.Any
, Enumerable.First
и т.д.).
Ответ 2
Во-первых, это удобная функция. Два, это позволяет вам делать ленивый возврат, а это означает, что он оценивается только при достижении значения. Это может быть бесценным в материалах, подобных запросу БД, или просто коллекции, которую вы не хотите полностью перебирать. В-третьих, это может быть быстрее в некоторых сценариях. Четыре, в чем разница? Вероятно, крошечная, поэтому микро оптимизация.
Ответ 3
Так как компилятор С# преобразует блоки итератора (yield return
) в конечный автомат. В этом случае состояние машины очень дорого.
Вы можете прочитать больше здесь: http://csharpindepth.com/articles/chapter6/iteratorblockimplementation.aspx
Ответ 4
Я использовал выход return, чтобы дать мне результаты по алгоритму. Каждый результат основан на предыдущем результате, но мне не нужны все они. Я использовал foreach с возвратом доходности, чтобы проверять каждый результат и прерывать цикл foreach, если я получаю результат, соответствующий моему требованию.
Алгоритм был достаточно сложным, поэтому я думаю, что для сохранения состояний между доходами доходности была достойная работа.
Я заметил, что он был на 3% -5% ниже, чем традиционный доход, но улучшение, которое я получаю, не нуждается в генерации всех результатов, намного больше, чем потеря производительности.
Ответ 5
.ToList()
, если необходимо, чтобы действительно завершить отложенную итерацию IEnumerable, препятствует измерению основной части.
По крайней мере, важно инициализировать список известным размером:
const int listSize = 2000000;
var tempList = новый List (listSize);
...
Список tempList = YieldReturnTest (listSize).ToList();
Примечание. Оба вызова заняли примерно одно и то же время на моей машине. Никакой разницы (Mono 4 на repl.it).