¿Por qué es Enumerable.Range más rápido que un ciclo de rendimiento directo?
-
03-07-2019 - |
Pregunta
El siguiente código está verificando el rendimiento de tres formas diferentes de hacer la misma solución.
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;
}
}
}
Los resultados son:
time = 306; result = 987459712
time = 1301; result = 987459712
time = 2860; result = 987459712
No es sorprendente que el " para bucle " es más rápido que las otras dos soluciones, porque Enumerable.Aggregate toma más invocaciones de métodos. Sin embargo, realmente me sorprende que " Enumerable.Range " es más rápido que el " hecho propio IEnumerable " ;. Pensé que Enumerable.Range tendría más gastos que el simple método GetIntRange.
¿Cuáles son las posibles razones para esto?
Solución
¿Por qué debería Enumerable.Range
ser más lento que su GetIntRange
hecho a sí mismo? De hecho, si Enumerable.Range
se definiera como
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;
}
}
}
entonces debería ser exactamente tan rápido como su GetIntRange
hecho a sí mismo. De hecho, esta es la implementación de referencia para Enumerable.Range
, sin ningún truco por parte del compilador o programador.
Es posible que desee comparar su GetIntRange
y System.Linq.Enumerable.Range
con la siguiente implementación (por supuesto, compilar en modo de lanzamiento, como señala Rob) . Esta implementación puede estar ligeramente optimizada con respecto a lo que generaría un compilador a partir de un bloque de iteradores.
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; } }
}
}
Otros consejos
Mi conjetura es que estás ejecutando en un depurador. Aquí están mis resultados, habiendo creado desde la línea de comandos con " / o + / debug- "
time = 142; result = 987459712
time = 1590; result = 987459712
time = 1792; result = 987459712
Todavía hay una ligera diferencia, pero no es tan pronunciada. Las implementaciones de Iterator Block no son tan eficientes como una solución hecha a la medida, pero son bastante buenas.
Suponiendo que se trata de una versión de compilación en ejecución, de lo contrario, todas las comparaciones están desactivadas, ya que el JIT no funcionará a toda máquina.
Puede mirar el ensamblaje con reflector y ver cuál es el rendimiento 'declaración se está ampliando también. El compilador creará una clase para encapsular el iterador. Tal vez haya más tareas de limpieza en el código generado que en la implementación de Enumerable.Range, que probablemente esté codificado a mano
Una ligera diferencia en la salida del Reflector (así como la verificación de argumentos y el nivel adicional de internalización definitivamente no son relevantes aquí). El código esencial es más como:
public static IEnumerable<int> Range(int start, int count) {
for(int current = 0; current < count; ++current) {
yield return start + current;
}
}
Es decir, en lugar de otra variable local, aplican una adición adicional para cada rendimiento.
He intentado comparar esto, pero no puedo detener suficientes procesos externos para obtener resultados comprensibles. También intenté cada prueba dos veces para ignorar los efectos del compilador JIT, pero incluso eso tiene resultados 'interesantes'.
Aquí hay una muestra de mis resultados:
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
y el código
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;
}
}
} }
compilado con
csc.exe -optimize+ -debug- RangeTests.cs