Gibt es einen Leistungsunterschied zwischen einer for-Schleife und einer for-each-Schleife?

184

Was ist, wenn überhaupt, der Leistungsunterschied zwischen den folgenden beiden Schleifen?

for (Object o: objectArrayList) {
    o.DoSomething();
}

und

for (int i=0; i<objectArrayList.size(); i++) {
    objectArrayList.get(i).DoSomething();
}
eflles
quelle
@ Keparo: Es ist eine "für jede" Schleife, keine "for-in" Schleife
Ande Turner
In Java heißt es "für jeden", aber wenn es um Ziel C geht, heißt es "für In" -Schleife.
DamithH
Die erweiterte Leistung für Schleifen wird hier erläutert
eckes
Selbst wenn es einen Unterschied geben würde, wäre dieser so gering, dass Sie die Lesbarkeit bevorzugen sollten, es sei denn, dieser Code wird Billionen Mal pro Sekunde ausgeführt. Und dann würden Sie einen geeigneten Benchmark benötigen, um eine vorzeitige Optimierung zu vermeiden.
Zabuzard

Antworten:

212

Aus Punkt 46 in Effective Java von Joshua Bloch:

Die in Version 1.5 eingeführte for-each-Schleife beseitigt die Unordnung und die Möglichkeit von Fehlern, indem der Iterator oder die Indexvariable vollständig ausgeblendet werden. Die resultierende Redewendung gilt gleichermaßen für Sammlungen und Arrays:

// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
    doSomething(e);
}

Wenn Sie den Doppelpunkt (:) sehen, lesen Sie ihn als "in". Daher lautet die obige Schleife "für jedes Element e in Elementen". Beachten Sie, dass die Verwendung der for-each-Schleife auch für Arrays keine Leistungseinbußen nach sich zieht. Tatsächlich kann es unter bestimmten Umständen einen leichten Leistungsvorteil gegenüber einer normalen for-Schleife bieten, da es die Grenze des Array-Index nur einmal berechnet. Während Sie dies von Hand tun können (Punkt 45), tun dies Programmierer nicht immer.

Vijay Dev
quelle
48
Erwähnenswert ist, dass es in einer for-each-Schleife keine Möglichkeit gibt, auf einen
Indexzähler
Ja, aber dieser Zähler ist jetzt außerhalb der Schleife sichtbar. Sicher, es ist eine einfache Lösung, aber das gilt auch für jeden!
Indolering
74
Es gibt die Leistungsbeeinträchtigung beim Zuweisen des Iterators. Ich hatte einen sehr parallelen Code in einem Android-Live-Hintergrundbild. Ich sah, dass der Müllsammler verrückt wurde. Dies lag daran, dass für jede Schleife temporäre Iteratoren in vielen verschiedenen (kurzlebigen) Threads zugewiesen wurden, was dem Garbage Collector viel Arbeit verursachte. Der Wechsel zu regulären indexbasierten Schleifen hat das Problem behoben.
Gsingh2011
1
@ gsingh2011 Dies hängt jedoch auch davon ab, ob Sie eine Direktzugriffsliste verwenden oder nicht. Die Verwendung des indexbasierten Zugriffs auf nicht wahlfreie Zugriffslisten ist viel schlechter als die Verwendung für Listen mit wahlfreiem Zugriff, denke ich. Wenn Sie mit der Schnittstellenliste arbeiten und daher den tatsächlichen Implementierungstyp nicht kennen, können Sie überprüfen, ob die Liste eine Instanz von (implementiert) RandomAccess ist, wenn Sie sich wirklich dafür interessieren: docs.oracle.com/javase/8 /docs/api/java/util/RandomAccess.html
Puce
3
@ gsingh2011 In den Android-Dokumenten ( developer.android.com/training/articles/perf-tips.html#Loops ) wird erwähnt, dass Sie Leistungseinbußen haben, wenn Sie foreach nur für eine ArrayList verwenden, nicht für andere Sammlungen. Ich bin gespannt, ob das dein Fall war.
Viccari
29

Alle diese Schleifen machen genau das Gleiche. Ich möchte sie nur zeigen, bevor ich meine zwei Cent einsetze.

Erstens die klassische Art, eine Liste zu durchlaufen:

for (int i=0; i < strings.size(); i++) { /* do something using strings.get(i) */ }

Zweitens der bevorzugte Weg, da er weniger fehleranfällig ist (wie oft haben SIE die Sache "oops, gemischt die Variablen i und j in diesen Schleifen innerhalb von Schleifen" gemacht?).

for (String s : strings) { /* do something using s */ }

Drittens die mikrooptimierte Schleife:

int size = strings.size();
for (int i = -1; ++i < size;) { /* do something using strings.get(i) */ }

Jetzt die tatsächlichen zwei Cent: Zumindest als ich diese testete, war der dritte der schnellste, wenn Millisekunden gezählt wurden, wie lange es für jeden Schleifentyp gedauert hat, wobei eine einfache Operation einige Millionen Mal wiederholt wurde - dies war Java 5 mit jre1.6u10 unter Windows, falls jemand interessiert ist.

Während es zumindest so zu sein scheint, dass der dritte der schnellste ist, sollten Sie sich wirklich fragen, ob Sie das Risiko eingehen möchten, diese Gucklochoptimierung überall in Ihrem Schleifencode zu implementieren, da nach meinem Dafürhalten keine tatsächliche Schleife vorliegt. Normalerweise der zeitaufwändigste Teil eines echten Programms (oder ich arbeite nur auf dem falschen Gebiet, wer weiß). Und auch, wie ich im Vorwand für die Java- for-each-Schleife erwähnt habe (einige bezeichnen sie als Iterator-Schleife und andere als for-in-Schleife ), ist es weniger wahrscheinlich, dass Sie diesen einen dummen Fehler treffen, wenn Sie sie verwenden. Und bevor Sie darüber diskutieren, wie dies sogar schneller sein kann als die anderen, denken Sie daran, dass javac den Bytecode überhaupt nicht optimiert (na ja, fast überhaupt nicht), sondern nur kompiliert.

Wenn Sie sich jedoch für Mikrooptimierung interessieren und / oder Ihre Software viele rekursive Schleifen verwendet, sind Sie möglicherweise am dritten Schleifentyp interessiert. Denken Sie daran, Ihre Software sowohl vor als auch nach dem Ändern der for-Schleifen auf diese seltsame, mikrooptimierte zu überprüfen.

P Arrayah
quelle
5
Bitte beachten Sie, dass die for-Schleife mit ++ i <= size "1-basiert" ist, z. B. wird die get-Methode innerhalb der Schleife für die Werte 1, 2, 3 usw. aufgerufen.
volley
15
Ein besserer Weg, um die mikrooptimierte Schleife zu schreiben, ist für (int i = 0, size = strings.size (); ++ i <= size;) {} Dies ist vorzuziehen, da dadurch der Umfang der Größe minimiert wird
Dónal
1
startet der dritte nicht beim ersten Durchlaufen der Schleife von i = 1 und überspringt das erste Element. und es ist unnötig, eine for-Schleife zu sein. int n = strings.length; while (n -> 0) {System.out.println ("" + n + "" + Strings [n]); }
Lassi Kinnunen
1
@ Dónal, dass das Schleifenmuster das erste verfehlt und ein IOOBE ergibt. Dieser funktioniert: für (int i = -1, size = list.size (); ++ i <size;)
Nathan Adams
1
"Alle diese Schleifen machen genau das Gleiche" ist falsch. Einer verwendet wahlfreien Zugriff get(int), der andere verwendet einen Iterator. Überlegen Sie, LinkedListwo die Leistung von for(int i=0;i<strings.size();i++) { /* do something using strings.get(i) */ }weitaus schlechter ist, da sie get(int)n-mal ausgeführt wird.
Steve Kuo
13

Die for-each-Schleife sollte im Allgemeinen bevorzugt werden. Der "get" -Ansatz ist möglicherweise langsamer, wenn die von Ihnen verwendete List-Implementierung keinen Direktzugriff unterstützt. Wenn beispielsweise eine LinkedList verwendet wird, entstehen Ihnen Durchquerungskosten, während für jeden Ansatz ein Iterator verwendet wird, der seine Position in der Liste verfolgt. Weitere Informationen zu den Nuancen der for-each-Schleife .

Ich denke, der Artikel ist jetzt hier: neuer Ort

Der hier gezeigte Link war tot.

Zach Scrivena
quelle
12

Nun, die Auswirkungen auf die Leistung sind größtenteils unbedeutend, aber nicht Null. Wenn Sie sich JavaDoc der RandomAccessSchnittstelle ansehen :

Als Faustregel sollte eine List-Implementierung diese Schnittstelle implementieren, wenn für typische Instanzen der Klasse diese Schleife gilt:

for (int i=0, n=list.size(); i < n; i++)
    list.get(i);

läuft schneller als diese Schleife:

for (Iterator i=list.iterator(); i.hasNext();)
      i.next();

Und für jede Schleife wird eine Version mit Iterator verwendet, sodass ArrayListbeispielsweise für jede Schleife nicht die schnellste ist.

Sarmun
quelle
Wirklich jeder? Sogar einer mit einem Array? Ich habe hier stackoverflow.com/questions/1006395/… gelesen, dass es sich nicht um einen Iterator handelt.
Ondra Žižka
@ OndraŽižka: Die for-each-Schleife verwendet Iteratoren beim Schleifen über Iterables, jedoch keine Arrays. Für Arrays wird eine for-Schleife mit einer Indexvariablen verwendet. Informationen dazu finden Sie im JLS .
Lii
Ja, Sie müssten es zuerst mit toArray in einem Array erstellen, was Kosten verursacht.
Lassi Kinnunen
6

Es scheint leider einen Unterschied zu geben.

Wenn Sie sich den generierten Bytecode für beide Arten von Schleifen ansehen, sind sie unterschiedlich.

Hier ist ein Beispiel aus dem Log4j-Quellcode.

In /log4j-api/src/main/java/org/apache/logging/log4j/MarkerManager.java haben wir eine statische innere Klasse namens Log4jMarker, die definiert:

    /*
     * Called from add while synchronized.
     */
    private static boolean contains(final Marker parent, final Marker... localParents) {
        //noinspection ForLoopReplaceableByForEach
        for (final Marker marker : localParents) {
            if (marker == parent) {
                return true;
            }
        }
        return false;
    }

Mit Standardschleife:

  private static boolean contains(org.apache.logging.log4j.Marker, org.apache.logging.log4j.Marker...);
    Code:
       0: iconst_0
       1: istore_2
       2: aload_1
       3: arraylength
       4: istore_3
       5: iload_2
       6: iload_3
       7: if_icmpge     29
      10: aload_1
      11: iload_2
      12: aaload
      13: astore        4
      15: aload         4
      17: aload_0
      18: if_acmpne     23
      21: iconst_1
      22: ireturn
      23: iinc          2, 1
      26: goto          5
      29: iconst_0
      30: ireturn

Mit für jeden:

  private static boolean contains(org.apache.logging.log4j.Marker, org.apache.logging.log4j.Marker...);
    Code:
       0: aload_1
       1: astore_2
       2: aload_2
       3: arraylength
       4: istore_3
       5: iconst_0
       6: istore        4
       8: iload         4
      10: iload_3
      11: if_icmpge     34
      14: aload_2
      15: iload         4
      17: aaload
      18: astore        5
      20: aload         5
      22: aload_0
      23: if_acmpne     28
      26: iconst_1
      27: ireturn
      28: iinc          4, 1
      31: goto          8
      34: iconst_0
      35: ireturn

Was ist mit DIESEM Oracle los?

Ich habe dies mit Java 7 und 8 unter Windows 7 versucht.

Gary Gregory
quelle
7
Für diejenigen, die versuchen, die Disassemblierung zu lesen, ist das Nettoergebnis, dass der in der Schleife generierte Code identisch ist, aber für jedes Setup eine zusätzliche temporäre Variable erstellt wurde, die einen Verweis auf das zweite Argument enthält. Wenn die zusätzliche versteckte Variable registriert ist, der Parameter selbst jedoch nicht während der Codegenerierung vorhanden ist, ist das for-each schneller. Wenn der Parameter im for (;;) - Beispiel registriert ist, ist die Ausführungszeit identisch. Muss ich Benchmark?
Robin Davies
4

Es ist immer besser, den Iterator zu verwenden, als zu indizieren. Dies liegt daran, dass der Iterator höchstwahrscheinlich für die List-Implementierung optimiert ist, während indiziert (Aufruf von get) möglicherweise nicht. Zum Beispiel ist LinkedList eine Liste, aber die Indizierung durch ihre Elemente ist langsamer als die Iteration mit dem Iterator.

Steve Kuo
quelle
10
Ich denke, es gibt kein "immer" in Leistungsoptimierungen.)
eckes
4

foreach macht die Absicht Ihres Codes klarer und dies wird normalerweise einer geringfügigen Geschwindigkeitsverbesserung vorgezogen - falls vorhanden.

Immer wenn ich eine indizierte Schleife sehe, muss ich sie etwas länger analysieren, um sicherzustellen, dass sie das tut, was ich denke. ZB Beginnt sie bei Null, schließt sie den Endpunkt ein oder aus usw.?

Die meiste Zeit verbringe ich damit, Code zu lesen (den ich geschrieben habe oder den jemand anderes geschrieben hat), und Klarheit ist fast immer wichtiger als Leistung. Es ist heutzutage einfach, die Leistung zu verwerfen, weil Hotspot so einen tollen Job macht.

Fortyrunner
quelle
4

Der folgende Code:

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;

interface Function<T> {
    long perform(T parameter, long x);
}

class MyArray<T> {

    T[] array;
    long x;

    public MyArray(int size, Class<T> type, long x) {
        array = (T[]) Array.newInstance(type, size);
        this.x = x;
    }

    public void forEach(Function<T> function) {
        for (T element : array) {
            x = function.perform(element, x);
        }
    }
}

class Compute {
    int factor;
    final long constant;

    public Compute(int factor, long constant) {
        this.factor = factor;
        this.constant = constant;
    }

    public long compute(long parameter, long x) {
        return x * factor + parameter + constant;
    }
}

public class Main {

    public static void main(String[] args) {
        List<Long> numbers = new ArrayList<Long>(50000000);
        for (int i = 0; i < 50000000; i++) {
            numbers.add(i * i + 5L);
        }

        long x = 234553523525L;

        long time = System.currentTimeMillis();
        for (int i = 0; i < numbers.size(); i++) {
            x += x * 7 + numbers.get(i) + 3;
        }
        System.out.println(System.currentTimeMillis() - time);
        System.out.println(x);
        x = 0;
        time = System.currentTimeMillis();
        for (long i : numbers) {
            x += x * 7 + i + 3;
        }
        System.out.println(System.currentTimeMillis() - time);
        System.out.println(x);
        x = 0;
        numbers = null;
        MyArray<Long> myArray = new MyArray<Long>(50000000, Long.class, 234553523525L);
        for (int i = 0; i < 50000000; i++) {
            myArray.array[i] = i * i + 3L;
        }
        time = System.currentTimeMillis();
        myArray.forEach(new Function<Long>() {

            public long perform(Long parameter, long x) {
                return x * 8 + parameter + 5L;
            }
        });
        System.out.println(System.currentTimeMillis() - time);
        System.out.println(myArray.x);
        myArray = null;
        myArray = new MyArray<Long>(50000000, Long.class, 234553523525L);
        for (int i = 0; i < 50000000; i++) {
            myArray.array[i] = i * i + 3L;
        }
        time = System.currentTimeMillis();
        myArray.forEach(new Function<Long>() {

            public long perform(Long parameter, long x) {
                return new Compute(8, 5).compute(parameter, x);
            }
        });
        System.out.println(System.currentTimeMillis() - time);
        System.out.println(myArray.x);
    }
}

Gibt folgende Ausgabe auf meinem System:

224
-699150247503735895
221
-699150247503735895
220
-699150247503735895
219
-699150247503735895

Ich verwende Ubuntu 12.10 alpha mit OracleJDK 1.7 Update 6.

Im Allgemeinen optimiert HotSpot viele Indirektionen und einfache redundante Vorgänge. Im Allgemeinen sollten Sie sich also keine Sorgen machen, es sei denn, es gibt viele davon in Folge oder sie sind stark verschachtelt.

Auf der anderen Seite ist das indizierte Abrufen von LinkedList viel langsamer als das Aufrufen des nächsten Iterators für LinkedList, sodass Sie diesen Leistungseinbruch vermeiden und gleichzeitig die Lesbarkeit beibehalten können, wenn Sie Iteratoren verwenden (explizit oder implizit in jeder Schleife).

Wibowit
quelle
3

Selbst bei so etwas wie einer ArrayList oder einem Vektor, bei dem "get" eine einfache Array-Suche ist, hat die zweite Schleife noch zusätzlichen Overhead, den die erste nicht hat. Ich würde erwarten, dass es ein bisschen langsamer ist als das erste.

Paul Tomblin
quelle
Die erste Schleife muss auch jedes Element erhalten. Dazu wird ein Iterator hinter den Kulissen erstellt. Sie sind wirklich gleichwertig.
Bill the Lizard
Wenn man in C denkt, kann ein Iterator nur einen Zeiger inkrementieren, aber ein get müsste den Wert von i jedes Mal mit der Breite eines Zeigers multiplizieren.
Paul Tomblin
Dies hängt davon ab, welche Art von Liste Sie verwenden. Ich denke, Sie haben Recht, get zu verwenden wäre niemals schneller und manchmal langsamer.
Bill the Lizard
3

Die einzige Möglichkeit, dies sicher zu wissen, besteht darin, es zu bewerten, und selbst das ist nicht so einfach, wie es sich anhört . Der JIT-Compiler kann sehr unerwartete Dinge mit Ihrem Code tun.

Jouni K. Seppänen
quelle
3

Hier ist eine kurze Analyse des Unterschieds, den das Android-Entwicklungsteam herausgestellt hat:

https://www.youtube.com/watch?v=MZOf3pOAM6A

Das Ergebnis ist , dass es ist ein Unterschied, und in sehr zurückhaltenden Umgebungen mit sehr großen Listen könnte es ein spürbarer Unterschied. Bei ihren Tests dauerte die für jede Schleife doppelt so lange. Ihre Tests wurden jedoch über eine Arrayliste von 400.000 ganzen Zahlen durchgeführt. Die tatsächliche Differenz pro Element im Array betrug 6 Mikrosekunden . Ich habe nicht getestet und sie haben es nicht gesagt, aber ich würde erwarten, dass der Unterschied bei der Verwendung von Objekten anstelle von Grundelementen etwas größer ist, aber auch dann, wenn Sie keinen Bibliothekscode erstellen, bei dem Sie keine Ahnung haben, in welchem ​​Umfang Sie gefragt werden Ich denke, der Unterschied ist es nicht wert, darüber nachzudenken.

TBridges42
quelle
2

Beim Variablennamen objectArrayListnehme ich an, dass dies eine Instanz von ist java.util.ArrayList. In diesem Fall wäre der Leistungsunterschied nicht wahrnehmbar.

Wenn es sich dagegen um eine Instanz von handelt java.util.LinkedList, ist der zweite Ansatz viel langsamer, da List#get(int)es sich um eine O (n) -Operation handelt.

Daher wird der erste Ansatz immer bevorzugt, es sei denn, der Index wird von der Logik in der Schleife benötigt.

Changgeng
quelle
1
1. for(Object o: objectArrayList){
    o.DoSomthing();
}
and

2. for(int i=0; i<objectArrayList.size(); i++){
    objectArrayList.get(i).DoSomthing();
}

Beide machen das Gleiche, aber für eine einfache und sichere Programmierung gibt es Möglichkeiten für fehleranfällige Anwendungen.

Santosh
quelle
1

Es ist seltsam, dass niemand das Offensichtliche erwähnt hat - foreach weist Speicher zu (in Form eines Iterators), während eine normale for-Schleife keinen Speicher zuweist. Bei Spielen unter Android ist dies ein Problem, da der Garbage Collector regelmäßig ausgeführt wird. In einem Spiel soll der Müllsammler nicht laufen ... NIEMALS. Verwenden Sie also keine foreach-Schleifen in Ihrer Zeichen- (oder Render-) Methode.

neuronale
quelle
1

Akzeptierte Antwort beantwortet die Frage, abgesehen vom Ausnahmefall von ArrayList ...

Da sich die meisten Entwickler auf ArrayList verlassen (zumindest glaube ich das)

Daher bin ich verpflichtet, hier die richtige Antwort hinzuzufügen.

Direkt aus der Entwicklerdokumentation: -

Die erweiterte for-Schleife (manchmal auch als "for-each" -Schleife bezeichnet) kann für Sammlungen verwendet werden, die die Iterable-Schnittstelle implementieren, sowie für Arrays. Bei Sammlungen wird ein Iterator zugewiesen, um Schnittstellenaufrufe an hasNext () und next () durchzuführen. Mit einer ArrayList ist eine handgeschriebene gezählte Schleife etwa dreimal schneller (mit oder ohne JIT), aber für andere Sammlungen entspricht die erweiterte for-Schleifensyntax genau der expliziten Verwendung von Iteratoren.

Es gibt verschiedene Alternativen zum Durchlaufen eines Arrays:

static class Foo {
    int mSplat;
}

Foo[] mArray = ...

public void zero() {
    int sum = 0;
    for (int i = 0; i < mArray.length; ++i) {
        sum += mArray[i].mSplat;
    }
}

public void one() {
    int sum = 0;
    Foo[] localArray = mArray;
    int len = localArray.length;

    for (int i = 0; i < len; ++i) {
        sum += localArray[i].mSplat;
    }
}

public void two() {
    int sum = 0;
    for (Foo a : mArray) {
        sum += a.mSplat;
    }
}

zero () ist am langsamsten, da die JIT die Kosten für das einmalige Abrufen der Array-Länge für jede Iteration durch die Schleife noch nicht optimieren kann.

one () ist schneller. Es zieht alles in lokale Variablen und vermeidet die Suche. Nur die Array-Länge bietet einen Leistungsvorteil.

two () ist für Geräte ohne JIT am schnellsten und für Geräte mit JIT nicht von one () zu unterscheiden. Es verwendet die erweiterte for-Schleifensyntax, die in Version 1.5 der Java-Programmiersprache eingeführt wurde.

Daher sollten Sie standardmäßig die erweiterte for-Schleife verwenden, jedoch eine handgeschriebene gezählte Schleife für die leistungskritische ArrayList-Iteration in Betracht ziehen.

Sreekanth Karumanaghat
quelle
-2

Ja, die for-eachVariante ist schneller als normal index-based-for-loop.

for-eachVariante verwendet iterator. Das Durchlaufen ist also schneller als eine normale forSchleife, die indexbasiert ist.
Dies liegt daran iterator, dass sie für das Durchlaufen optimiert sind, da sie kurz vor dem nächsten Element und unmittelbar nach dem vorherigen Element zeigen . Einer der Gründe dafür, index-based-for-looplangsam zu sein, ist, dass es jedes Mal , wenn es nicht mit dem ist, berechnen und sich zur Elementposition bewegen mussiterator .

cse
quelle
-3
public class FirstJavaProgram {

    public static void main(String[] args) 
    {
        int a[]={1,2,3,45,6,6};

// Method 1: this is simple way to print array 

        for(int i=0;i<a.length;i++) 
        { 
            System.out.print(a[i]+" ");
        }

// Method 2: Enhanced For loop

        for(int i:a)
        {
            System.out.print(i+" ");
        }
    }
}
SS Tiwari
quelle