Diskussionen om præferenceforskellen mellem FOREACH og FOR er ikke ny. Vi ved alle, at FOREACH er langsommere, men ikke alle ved hvorfor.
Da jeg begyndte at lære .NET, fortalte en person mig, at FOREACH er to gange langsommere end FOR. Det sagde han uden grund. Jeg tog det for givet.
Til sidst besluttede jeg mig for at udforske forskellen i FOREACH og FOR loop ydeevne og skrive denne artikel for at diskutere nuancer.
Lad os tage et kig på følgende kode:
foreach (var item in Enumerable.Range(0, 128))
{
Console.WriteLine(item);
}
FOREACH er et syntakssukker. I dette særlige tilfælde transformerer compileren den til følgende kode:
IEnumerator<int> enumerator = Enumerable.Range(0, 128).GetEnumerator();
try
{
while (enumerator.MoveNext())
{
int item = enumerator.Current;
Console.WriteLine(item);
}
}
finally
{
if (enumerator != null)
{
enumerator.Dispose();
}
}
Når vi ved dette, kan vi antage årsagen til, hvorfor FOREACH er langsommere end FOR:
- Et nyt objekt er ved at blive oprettet. Det kaldes Skaber.
- MoveNext-metoden kaldes på hver iteration.
- Hver iteration giver adgang til den aktuelle egenskab.
Det er det! Det hele er dog ikke så nemt, som det lyder.
Heldigvis (eller desværre) kan C#/CLR muligvis udføre optimeringer under kørslen. Fordelen er, at koden virker hurtigere. Ulempen – udviklere bør være opmærksomme på disse optimeringer.
Arrayet er en type, der er dybt integreret i CLR, og CLR giver en række optimeringer til denne type. FOREACH-løkken er en gentagende enhed, hvilket er et nøgleaspekt af præstationen. Senere i artiklen vil vi diskutere, hvordan man itererer gennem arrays og lister ved hjælp af den statiske Array.ForEach-metode og List.ForEach-metoden.
Testmetoder
static double ArrayForWithoutOptimization(int[] array)
{
int sum = 0;
var watch = Stopwatch.StartNew();
for (int i = 0; i < array.Length; i++)
sum += array[i];
watch.Stop();
return watch.Elapsed.TotalMilliseconds;
}
static double ArrayForWithOptimization(int[] array)
{
int length = array.Length;
int sum = 0;
var watch = Stopwatch.StartNew();
for (int i = 0; i < length; i++)
sum += array[i];
watch.Stop();
return watch.Elapsed.TotalMilliseconds;
}
static double ArrayForeach(int[] array)
{
int sum = 0;
var watch = Stopwatch.StartNew();
foreach (var item in array)
sum += item;
watch.Stop();
return watch.Elapsed.TotalMilliseconds;
}
static double ArrayForEach(int[] array)
{
int sum = 0;
var watch = Stopwatch.StartNew();
Array.ForEach(array, i => { sum += i; });
watch.Stop();
return watch.Elapsed.TotalMilliseconds;
}
Testbetingelser:
- Indstillingen "Optimer kode" er slået til.
- Antallet af elementer er lig med 100 000 000 (både i arrayet og listen).
- PC-specifikation:Intel Core i-5 og 8 GB RAM.
Arrays
Diagrammet viser, at FOR og FOREACH bruger den samme mængde tid, mens de itererer gennem arrays. Og det er fordi CLR-optimering konverterer FOREACH til FOR og bruger længden af arrayet som den maksimale iterationsgrænse. Det er lige meget, om arraylængden er cachelagret eller ej (når du bruger FOR), resultatet er næsten det samme.
Det lyder måske mærkeligt, men cachelægning af array-længden kan påvirke ydeevnen. Mens du bruger array .Længde som iterationsgrænsen tester JIT indekset for at ramme den højre kant ud over cyklussen. Denne kontrol udføres kun én gang.
Det er meget nemt at ødelægge denne optimering. Tilfældet, hvor variablen er cachelagret, er næppe optimeret.
Array.foreach viste de værste resultater. Implementeringen er ret enkel:
public static void ForEach<T>(T[] array, Action<T> action)
{
for (int index = 0; index < array.Length; ++index)
action(array[index]);
}
Hvorfor kører det så langsomt? Den bruger FOR under hætten. Nå, grunden er at ringe til ACTION-delegerede. Faktisk kaldes der en metode på hver iteration, som reducerer ydeevnen. Desuden kaldes de delegerede ikke så hurtigt, som vi ønsker.
Lister
Resultatet er et helt andet. Ved iteration af lister viser FOR og FOREACH forskellige resultater. Der er ingen optimering. FOR (med caching af listens længde) viser det bedste resultat, hvorimod FOREACH er mere end 2 gange langsommere. Det er fordi den omhandler MoveNext og Current under motorhjelmen. List.ForEach såvel som Array.ForEach viser det dårligste resultat. Delegerede kaldes altid virtuelt. Implementeringen af denne metode ser således ud:
public void ForEach(Action<T> action)
{
int num = this._version;
for (int index = 0; index < this._size && num == this._version; ++index)
action(this._items[index]);
if (num == this._version)
return;
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}
Hver iteration kalder handlingsdelegaten. Den kontrollerer også, om listen er ændret, og hvis det er tilfældet, bliver der kastet en undtagelse.
List bruger internt en array-baseret model, og ForEach-metoden bruger array-indekset til at iterere igennem, hvilket er betydeligt hurtigere end at bruge indekseren.
Specifikke tal
- FO-løkken uden længdecaching og FOREACH fungerer lidt hurtigere på arrays end FOR med længdecaching.
- Array.Foreach ydeevne er cirka 6 gange langsommere end FOR / FOREACH ydeevne.
- FO-løkken uden længdecaching fungerer 3 gange langsommere på lister sammenlignet med arrays.
- FO-løkken med længdecaching fungerer 2 gange langsommere på lister sammenlignet med arrays.
- Foreach-løkken fungerer 6 gange langsommere på lister sammenlignet med arrays.
Her er en rangliste for lister:
Og for arrays:
Konklusion
Jeg nød virkelig denne undersøgelse, især skriveprocessen, og jeg håber, at du også har nydt det. Som det viste sig, er FOREACH hurtigere på arrays end FOR med længdejagende. På listestrukturer er FOREACH langsommere end FOR.
Koden ser bedre ud, når du bruger FOREACH, og moderne processorer tillader brugen af den. Men hvis du har brug for at optimere din kodebase meget, er det bedre at bruge FOR.
Hvad synes du, hvilken loop kører hurtigere, FOR eller FOREACH?