Почему Enumerable.Range быстрее, чем прямой цикл возврата?
Нижеприведенный код проверяет производительность трех разных способов выполнения одного и того же решения.
public static void Main(string[] args)
{
// for loop
{
Stopwatch sw = Stopwatch.StartNew();
int accumulator = 0;
for (int i = 1; i <= 100000000; ++i)
{
accumulator += i;
}
sw.Stop();
Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, accumulator);
}
//Enumerable.Range
{
Stopwatch sw = Stopwatch.StartNew();
var ret = Enumerable.Range(1, 100000000).Aggregate(0, (accumulator, n) => accumulator + n);
sw.Stop();
Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
}
//self-made IEnumerable<int>
{
Stopwatch sw = Stopwatch.StartNew();
var ret = GetIntRange(1, 100000000).Aggregate(0, (accumulator, n) => accumulator + n);
sw.Stop();
Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
}
}
private static IEnumerable<int> GetIntRange(int start, int count)
{
int end = start + count;
for (int i = start; i < end; ++i)
{
yield return i;
}
}
}
Результаты:
time = 306; result = 987459712
time = 1301; result = 987459712
time = 2860; result = 987459712
Неудивительно, что цикл "for" быстрее, чем два других решения, потому что Enumerable.Aggregate принимает больше вызовов методов. Однако меня действительно удивляет, что "Enumerable.Range" быстрее, чем "самодельный IEnumerable". Я думал, что Enumerable.Range будет иметь больше накладных расходов, чем простой метод GetIntRange.
Каковы возможные причины этого?
Ответы
Ответ 1
Почему Enumerable.Range
должен быть медленнее, чем ваш самодельный GetIntRange
? На самом деле, если Enumerable.Range
были определены как
public static class Enumerable {
public static IEnumerable<int> Range(int start, int count) {
var end = start + count;
for(var current = start; current < end; ++current) {
yield return current;
}
}
}
то это должно быть точно так же быстро, как ваш самодельный GetIntRange
. Это фактически эталонная реализация для Enumerable.Range
, без каких-либо трюков со стороны компилятора или программиста.
Вы можете сравнить свои GetIntRange
и System.Linq.Enumerable.Range
со следующей реализацией (конечно, скомпилировать в режиме выпуска, как указывает Rob). Эта реализация может быть слегка оптимизирована в отношении того, что компилятор будет генерировать из блока итератора.
public static class Enumerable {
public static IEnumerable<int> Range(int start, int count) {
return new RangeEnumerable(start, count);
}
private class RangeEnumerable : IEnumerable<int> {
private int _Start;
private int _Count;
public RangeEnumerable(int start, int count) {
_Start = start;
_Count = count;
}
public virtual IEnumerator<int> GetEnumerator() {
return new RangeEnumerator(_Start, _Count);
}
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
}
private class RangeEnumerator : IEnumerator<int> {
private int _Current;
private int _End;
public RangeEnumerator(int start, int count) {
_Current = start - 1;
_End = start + count;
}
public virtual void Dispose() {
_Current = _End;
}
public virtual void Reset() {
throw new NotImplementedException();
}
public virtual bool MoveNext() {
++_Current;
return _Current < _End;
}
public virtual int Current { get { return _Current; } }
object IEnumerator.Current { get { return Current; } }
}
}
Ответ 2
Я предполагаю, что вы работаете в отладчике. Вот мои результаты, построив из командной строки "/o +/debug -"
time = 142; result = 987459712
time = 1590; result = 987459712
time = 1792; result = 987459712
Есть еще небольшая разница, но она не так выражена. Реализации блоков итератора не так эффективны, как индивидуальное решение, но они довольно хороши.
Ответ 3
Предполагая, что это запуск релиза, в противном случае все сравнения отключены, так как JIT не будет работать.
Вы можете посмотреть на сборку с reflector и посмотреть, как расширяется инструкция 'yield'. Компилятор будет создавать класс для инкапсуляции итератора. Возможно, в сгенерированном коде происходит более домашняя работа, чем реализация Enumerable.Range, которая, скорее всего, ручная кодировка
Ответ 4
Небольшое различие в выходе рефлектора (а также проверка аргументов и дополнительный уровень интернализации определенно не актуальны здесь). Основной код больше похож:
public static IEnumerable<int> Range(int start, int count) {
for(int current = 0; current < count; ++current) {
yield return start + current;
}
}
То есть вместо другой локальной переменной они применяют дополнительное добавление для каждого выхода.
Я попытался сравнить это, но я не могу остановить достаточно внешних процессов, чтобы получить понятные результаты. Я также дважды тестировал каждый тест, чтобы игнорировать эффекты компилятора JIT, но даже это имеет "интересные" результаты.
Вот пример моих результатов:
Run 0:
time = 4149; result = 405000000450000000
time = 25645; result = 405000000450000000
time = 39229; result = 405000000450000000
time = 29872; result = 405000000450000000
time = 4277; result = 405000000450000000
time = 26878; result = 405000000450000000
time = 26333; result = 405000000450000000
time = 26684; result = 405000000450000000
Run 1:
time = 4063; result = 405000000450000000
time = 22714; result = 405000000450000000
time = 34744; result = 405000000450000000
time = 26954; result = 405000000450000000
time = 4033; result = 405000000450000000
time = 26657; result = 405000000450000000
time = 25855; result = 405000000450000000
time = 25031; result = 405000000450000000
Run 2:
time = 4021; result = 405000000450000000
time = 21815; result = 405000000450000000
time = 34304; result = 405000000450000000
time = 32040; result = 405000000450000000
time = 3993; result = 405000000450000000
time = 24779; result = 405000000450000000
time = 29275; result = 405000000450000000
time = 32254; result = 405000000450000000
и код
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
namespace RangeTests
{
class TestRange
{
public static void Main(string[] args)
{
for(int l = 1; l <= 2; ++l)
{
const int N = 900000000;
System.GC.Collect(2);
// for loop
{
Stopwatch sw = Stopwatch.StartNew();
long accumulator = 0;
for (int i = 1; i <= N; ++i)
{
accumulator += i;
}
sw.Stop();
Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, accumulator);
}
System.GC.Collect(2);
//Enumerable.Range
{
Stopwatch sw = Stopwatch.StartNew();
var ret = Enumerable.Range(1, N).Aggregate(0, (long accumulator,int n) => accumulator + n);
sw.Stop();
Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
}
System.GC.Collect(2);
//self-made IEnumerable<int>
{
Stopwatch sw = Stopwatch.StartNew();
var ret = GetIntRange(1, N).Aggregate(0, (long accumulator,int n) => accumulator + n);
sw.Stop();
Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
}
System.GC.Collect(2);
//self-made adjusted IEnumerable<int>
{
Stopwatch sw = Stopwatch.StartNew();
var ret = GetRange(1, N).Aggregate(0, (long accumulator,int n) => accumulator + n);
sw.Stop();
Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
}
System.GC.Collect(2);
Console.WriteLine();
} }
private static IEnumerable<int> GetIntRange(int start, int count)
{
int end = start + count;
for (int i = start; i < end; ++i)
{
yield return i;
}
}
private static IEnumerable<int> GetRange(int start, int count)
{
for (int i = 0; i < count; ++i)
{
yield return start + i;
}
}
} }
скомпилирован с
csc.exe -optimize+ -debug- RangeTests.cs