Diskussionen om preferensskillnaden mellan FOREACH och FOR är inte ny. Vi vet alla att FOREACH är långsammare, men alla vet inte varför.
När jag började lära mig .NET sa en person till mig att FOREACH är två gånger långsammare än FOR. Han sa detta utan några skäl. Jag tog det för givet.
Så småningom bestämde jag mig för att utforska skillnaden i FOREACH och FOR loop prestanda och skriva den här artikeln för att diskutera nyanser.
Låt oss ta en titt på följande kod:
foreach (var item in Enumerable.Range(0, 128))
{
Console.WriteLine(item);
}
FOREACH är ett syntaxsocker. I det här specifika fallet omvandlar kompilatorn den till följande kod:
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();
}
}
Genom att veta detta kan vi anta orsaken till varför FOREACH är långsammare än FOR:
- Ett nytt objekt skapas. Det kallas Skapare.
- MoveNext-metoden anropas vid varje iteration.
- Varje iteration får åtkomst till den aktuella egenskapen.
Det är allt! Men allt är inte så lätt som det låter.
Lyckligtvis (eller tyvärr) kan C#/CLR utföra optimeringar under körning. Fördelen är att koden fungerar snabbare. Nackdelen – utvecklare bör vara medvetna om dessa optimeringar.
Arrayen är en typ som är djupt integrerad i CLR, och CLR tillhandahåller ett antal optimeringar för denna typ. FOREACH-slingan är en itererbar enhet, vilket är en nyckelaspekt av framförandet. Senare i artikeln kommer vi att diskutera hur man itererar genom arrayer och listor med hjälp av den statiska Array.ForEach-metoden och 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;
}
Testförhållanden:
- Alternativet "Optimera kod" är aktiverat.
- Antalet element är lika med 100 000 000 (både i arrayen och listan).
- PC-specifikation:Intel Core i-5 och 8 GB RAM.
Arrayer
Diagrammet visar att FOR och FOREACH spenderar samma tid medan de itererar genom arrayer. Och det beror på att CLR-optimering konverterar FOREACH till FOR och använder längden på arrayen som den maximala iterationsgränsen. Det spelar ingen roll om arraylängden är cachad eller inte (när du använder FOR), resultatet är nästan detsamma.
Det kan låta konstigt, men cachelagring av arraylängden kan påverka prestandan. När du använder array .Längd som iterationsgränsen, JIT testar indexet för att träffa den högra gränsen bortom cykeln. Denna kontroll utförs endast en gång.
Det är väldigt lätt att förstöra denna optimering. Fallet när variabeln cachelagras är knappast optimerat.
Array.foreach visade de sämsta resultaten. Dess implementering är ganska enkel:
public static void ForEach<T>(T[] array, Action<T> action)
{
for (int index = 0; index < array.Length; ++index)
action(array[index]);
}
Varför går det så långsamt då? Den använder FOR under huven. Jo, anledningen är att ringa ACTION-delegaten. Faktum är att en metod anropas på varje iteration, vilket minskar prestandan. Dessutom anropas delegaterna inte så snabbt som vi skulle vilja.
Listor
Resultatet är ett helt annat. Vid iteration av listor visar FOR och FOREACH olika resultat. Det finns ingen optimering. FOR (med cachning av listans längd) visar det bästa resultatet, medan FOREACH är mer än 2 gånger långsammare. Det är för att det handlar om MoveNext och Current under huven. List.ForEach såväl som Array.ForEach visar det sämsta resultatet. Delegater kallas alltid virtuellt. Implementeringen av denna metod ser ut så här:
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);
}
Varje iteration anropar åtgärdsombudet. Den kontrollerar också om listan ändras och i så fall skapas ett undantag.
List använder internt en arraybaserad modell och ForEach-metoden använder arrayindexet för att iterera igenom, vilket är betydligt snabbare än att använda indexeraren.
Specifika nummer
- FÖR-slingan utan längdcache och FOREACH fungerar något snabbare på arrayer än FOR med längdcache.
- Array.Foreach prestanda är ungefär 6 gånger långsammare än FOR / FOREACH-prestanda.
- FO-slingan utan längdcache fungerar 3 gånger långsammare på listor, jämfört med arrayer.
- FO-slingan med längdcache fungerar 2 gånger långsammare på listor, jämfört med arrayer.
- Foreach-slingan fungerar 6 gånger långsammare på listor, jämfört med arrayer.
Här är en topplista för listor:
Och för arrayer:
Slutsats
Jag gillade verkligen den här undersökningen, särskilt skrivprocessen, och jag hoppas att du också har njutit av den. Som det visade sig är FOREACH snabbare på arrayer än FOR med längdjagning. På liststrukturer är FOREACH långsammare än FOR.
Koden ser bättre ut när du använder FOREACH, och moderna processorer tillåter användning av den. Men om du behöver optimera din kodbas i hög grad är det bättre att använda FOR.
Vad tycker du, vilken loop går snabbare, FOR eller FOREACH?