Pourquoi Enumerable.Range est-il plus rapide qu'une boucle directe de rendement?
-
03-07-2019 - |
Question
Le code ci-dessous vérifie les performances de trois manières différentes de réaliser la même solution.
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;
}
}
}
Les résultats sont:
time = 306; result = 987459712
time = 1301; result = 987459712
time = 2860; result = 987459712
Il n’est pas surprenant que la boucle "for for" est plus rapide que les deux autres solutions, car Enumerable.Aggregate prend plus d'appels de méthode. Cependant, cela me surprend vraiment que " Enumerable.Range " est plus rapide que le "IEnumerable self-made". Je pensais que Enumerable.Range aurait plus de temps système que la simple méthode GetIntRange.
Quelles en sont les raisons possibles?
La solution
Pourquoi Enumerable.Range
devrait-il être plus lent que votre GetIntRange
que vous avez créé vous-même? En fait, si Enumerable.Range
était défini comme
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;
}
}
}
alors il devrait être exactement aussi rapide que votre propre GetIntRange
. C’est en fait l’implémentation de référence pour Enumerable.Range
, en l’absence de ruse de la part du compilateur ou du programmeur.
Vous souhaiterez peut-être comparer votre GetIntRange
et votre System.Linq.Enumerable.Range
avec l'implémentation suivante (bien sûr, compilez en mode édition, comme le souligne Rob) . Cette implémentation peut être légèrement optimisée par rapport à ce qu'un compilateur générerait à partir d'un bloc itérateur.
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; } }
}
}
Autres conseils
Je suppose que vous exécutez un débogueur. Voici mes résultats, construits à partir de la ligne de commande avec "/ o + / debug-"
time = 142; result = 987459712
time = 1590; result = 987459712
time = 1792; result = 987459712
Il y a encore une légère différence, mais elle n’est pas aussi prononcée. Les implémentations de bloc Iterator ne sont pas aussi efficaces qu'une solution sur mesure, mais elles sont plutôt efficaces.
En supposant qu'il s'agisse d'une version validée en cours d'exécution, sinon toutes les comparaisons sont fausses car le JIT ne fonctionnera pas à fond.
Vous pouvez regarder l'assemblage avec réflecteur et voir ce que vous obtenez 'déclaration est également élargie. Le compilateur va créer une classe pour encapsuler l'itérateur. Le code généré contient peut-être plus d’entretien courant que l’implémentation de Enumerable.Range, qui est probablement codée à la main
Une légère différence dans la sortie du réflecteur (ainsi que le contrôle des arguments et le niveau supplémentaire d'internalisation ne sont absolument pas pertinents ici). Le code essentiel ressemble plus à:
public static IEnumerable<int> Range(int start, int count) {
for(int current = 0; current < count; ++current) {
yield return start + current;
}
}
C'est-à-dire qu'au lieu d'utiliser une autre variable locale, ils ajoutent un supplément pour chaque rendement.
J’ai essayé d’évaluer cela, mais je ne peux pas arrêter suffisamment de processus externes pour obtenir des résultats compréhensibles. J'ai également essayé deux fois chaque test pour ignorer les effets du compilateur JIT, mais même cela a des résultats "intéressants".
Voici un exemple de mes résultats:
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
et le code
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;
}
}
} }
compilé avec
csc.exe -optimize+ -debug- RangeTests.cs