For döngüsü ve for-each döngüsü arasında bir performans farkı var mı?


184

Varsa, aşağıdaki iki döngü arasındaki performans farkı nedir?

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

ve

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

@Keparo: Bu "her biri için" bir döngü değil "bir" giriş döngüsü
Ande Turner

Java'da "her biri için" denir, ancak Hedef C'ye gelince "In" döngüsü denir.
damithH


Döngü performansı için genişletilmiş burada tartışılmıştır: stackoverflow.com/questions/12155987/…
eckes

Bir fark olsa bile , bu kod parçası saniyede trilyonlarca kez yürütülmediği sürece okunabilirliği tercih etmelisiniz. Ve yine de erken optimizasyondan kaçınmak için uygun bir karşılaştırmaya ihtiyacınız olacaktır.
Zabuzard

Yanıtlar:


212

Joshua Bloch'un Etkili Java'sındaki Madde 46'dan :

1.5 sürümünde tanıtılan for-each döngü, yineleyiciden ve yineleyici veya dizin değişkenini tamamen gizleyerek hata fırsatından kurtulur. Ortaya çıkan deyim, koleksiyonlar ve diziler için eşit olarak geçerlidir:

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

İki nokta üst üste işaretini (:) gördüğünüzde, "inç" olarak okuyun. Böylece, yukarıdaki döngü “elemanlardaki her e öğesi için” olarak okunur. Diziler için bile, her biri için for döngüsünü kullanmanın performans cezası olmadığını unutmayın. Aslında, dizi endeksinin sınırını sadece bir kez hesapladığı için, bazı durumlarda sıradan bir döngü için hafif bir performans avantajı sunabilir. Bunu elinizle yapabilirsiniz (Madde 45), programcılar bunu her zaman yapmaz.


48
Her bir döngü için bir indeks sayacına erişmenin bir yolu olmadığını belirtmek gerekir (çünkü mevcut değildir)
basszero

Evet, ancak bu sayaç artık döngünün dışında görülebilir. Tabii, bu basit bir düzeltme ama her biri için de öyle!
Indolering

74
Yineleyiciyi ayırmanın performans cezası vardır. Android canlı duvar kağıdında oldukça paralel bir kodum vardı. Çöp toplayıcının delirdiğini gördüm. Çünkü her döngü, birçok farklı (kısa ömürlü) iş parçacığında geçici yineleyiciler tahsis ediyordu, çöp toplayıcıya çok fazla iş kattıyordu. Normal dizin tabanlı döngülere geçmek sorunu çözdü.
gsingh2011

1
@ gsingh2011 Ancak bu, rasgele erişim listesi kullanıp kullanmadığınıza da bağlıdır. Rastgele olmayan erişim listelerine dizin tabanlı erişim kullanmak, rasgele erişim listeleri olan her biri için kullanmaktan çok daha kötü olacaktır. Arayüz Listesi ile çalışıyorsanız ve böylece gerçek uygulama türünü bilmiyorsanız, listenin RandomAccess'in
Puce

3
@ gsingh2011 Android dokümanları ( developer.android.com/training/articles/perf-tips.html#Loops ), diğer koleksiyonlarda değil, yalnızca ArrayList üzerinden foreach kullanırken performans cezası alacağınızdan bahseder. Senin durumun bu olup olmadığını merak ediyorum.
Viccari

29

Tüm bu döngüler aynısını yapıyor, sadece iki sentime atmadan önce bunları göstermek istiyorum.

İlk olarak, List'te döngü yapmanın klasik yolu:

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

İkincisi, daha az hataya yatkın olduğu için tercih edilen yol (kaç kez SİZİN "oops, döngüler içindeki bu döngülerdeki i ve j değişkenlerini karıştırdık" şeyini kaç kez yaptınız?).

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

Üçüncü olarak, döngü için mikro optimize edilmiş:

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

Şimdi gerçek iki sent: En azından bunları test ederken, üçüncüsü, her bir döngü türü için birkaç milyon kez tekrarlanan basit bir işlemle milisaniye sürdüğü zaman en hızlıydı - bu Java 5'i kullanıyordu. Herkes ilgilenirse Windows üzerinde jre1.6u10 ile.

En azından üçüncü olan en hızlı olacak gibi görünse de, gerçekten kendinize bu peephole optimizasyonunu döngü kodunuzdaki her yerde uygulama riskini almak isteyip istemediğinizi sormalısınız, çünkü gerçek döngü değil ' t Genellikle herhangi bir gerçek programın en çok zaman alan kısmı (veya belki de yanlış alanda çalışıyorum, kim bilir) Ayrıca, her bir döngü için Java'nın bahanesinde de bahsettiğim gibi (bazıları Iterator döngüsü ve diğerleri için de döngü olarak adlandırılır ) kullanırken bu belirli aptal hatayı vurma olasılığınız daha düşüktür. Ve bunun bile diğerlerinden bile daha hızlı olabileceğini tartışmadan önce, javacın bayt kodunu hiç optimize etmediğini (neredeyse, neredeyse her durumda) unutmayın.

Yine de mikro optimizasyon yapıyorsanız ve / veya yazılımınız çok sayıda özyinelemeli döngü kullanıyorsa ve bu durumda üçüncü döngü türüyle ilgilenebilirsiniz. Sadece bu tek, mikro optimize edilmiş döngüye sahip olduğunuz döngüleri değiştirmeden önce ve sonra yazılımınızı iyi bir şekilde karşılaştırmayı unutmayın.


5
++ i <= boyutuna sahip for döngüsünün "1 tabanlı" olduğunu unutmayın, örn. Döngü içindeki get-yöntemi 1, 2, 3 vb. Değerler için çağrılır.
volley

15
Mikro optimize edilmiş döngüyü yazmanın daha iyi bir yolu şudur (int i = 0, size = strings.size (); ++ i <= size;) {} Bu, boyut kapsamını en aza indirdiği için tercih edilir
Dónal

1
üçüncüsü, ilk öğeyi atlayarak döngüden ilk kez geçtiğinde i = 1'den başlamaz. ve for döngüsü olmak gereksizdir. int n = strings.length; (n -> 0) {System.out.println ("" + n + "" + dizeler [n]); }
Lassi Kinnunen

1
@ Döngü deseni ilkini özlüyor ve bir IOOBE veriyor. Bu işe yarıyor: (int i = -1, size = list.size (); ++ i <size;)
Nathan Adams

1
"Tüm bu döngüler aynısını yapıyor" yanlış. Biri rasgele erişim get(int), diğeri an Iterator. N kez LinkedListyaptığı için performansının for(int i=0;i<strings.size();i++) { /* do something using strings.get(i) */ }çok daha kötü olduğunu düşünün get(int).
Steve Kuo

13

Her biri için döngü genel olarak tercih edilmelidir. Kullandığınız Liste uygulaması rasgele erişimi desteklemiyorsa "get" yaklaşımı daha yavaş olabilir. Örneğin, LinkedList kullanılırsa, geçiş maliyetine maruz kalırsınız, buna karşılık her yaklaşım için listedeki konumunu izleyen bir yineleyici kullanılır. Her bir döngü için nüanslar hakkında daha fazla bilgi .

Bence makale şimdi burada: yeni konum

Burada gösterilen bağlantı ölmüştü.


12

Performans etkisi çoğunlukla önemsizdir, ancak sıfır değildir. RandomAccessArayüzün JavaDoc'una bakarsanız :

Genel bir kural olarak, List uygulamasının, sınıfın tipik örnekleri için bu döngü aşağıdaki durumlarda bu arabirimi uygulamalıdır:

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

bu döngüden daha hızlı çalışır:

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

Ve for-each döngü yineleyicili sürüm kullanıyor, bu nedenle ArrayListörneğin for-loop en hızlı değil.


Gerçekten mi? Dizisi olan biri bile? Burada bir yineleyici içermediğini stackoverflow.com/questions/1006395/… okudum .
Ondra Žižka

@ OndraŽižka: for-her döngü Iterables üzerinde döngü yaparken yineleyiciler kullanır , ancak diziler kullanmaz. Diziler için dizin değişkenli bir for-loop kullanılır. JLS'de bu konuda bilgi var .
Lii

evet bu yüzden önce bir maliyeti olan toArray ile bir dizi oluşturmak gerekir.
Lassi Kinnunen

6

Maalesef bir fark var gibi görünüyor.

Her iki döngü türü için oluşturulan bayt koduna bakarsanız, bunlar farklıdır.

İşte Log4j kaynak kodundan bir örnek.

/Log4j-api/src/main/java/org/apache/logging/log4j/MarkerManager.java'da Log4jMarker adında statik bir iç sınıfımız var.

    /*
     * 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;
    }

Standart döngü ile:

  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

Her biri için:

  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

THAT Oracle'ın durumu nedir?

Bunu Windows 7'de Java 7 ve 8 ile denedim.


7
Demontajı okumaya çalışanlar için, net sonuç, döngü içinde oluşturulan kodun aynı olması, ancak for-setup'ın ikinci argümana başvuru içeren ekstra geçici bir değişken oluşturduğu görülüyor. Ek gizli değişken kaydedilmişse, ancak parametrenin kendisi kod oluşturma sırasında değilse, for-each daha hızlı olacaktır; parametre for (;;) örneğinde kayıtlıysa, yürütme süresi aynı olur. Kıyaslama mı gerekiyor?
Robin Davies

4

İndeksleme yerine yineleyiciyi kullanmak her zaman daha iyidir. Bunun nedeni, dizin oluşturucu (get çağrısında bulunma) olmayabilirken yineleyicinin büyük olasılıkla Liste uygulaması için en iyi duruma getirilmesidir. Örneğin LinkedList bir Liste'dir, ancak öğeleri arasında endeksleme yineleyici kullanılarak yinelemekten daha yavaş olacaktır.


10
Performans optimizasyonlarında "her zaman" diye bir şey olmadığını düşünüyorum.)
15:11

4

foreach, kodunuzun amacını daha net hale getirir ve normalde çok küçük bir hız iyileştirmesi yerine tercih edilir - eğer varsa.

Ne zaman indeksli bir döngü görüyorum ben onun ne düşündüğünü yapar emin olmak için biraz daha ayrıştırmak gerekir Eg sıfırdan başlıyor, bitiş noktası dahil veya hariç mi?

Zamanımın çoğunu kod okumak için harcadığım anlaşılıyor (yazdığım veya başka birisinin yazdığı) ve netlik neredeyse her zaman performanstan daha önemlidir. Hotspot böylesine inanılmaz bir iş çıkardığı için bugünlerde performansı kapatmak kolay.


4

Aşağıdaki kod:

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);
    }
}

Sistemimde aşağıdaki çıktıyı verir:

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

OracleJDK 1.7 güncelleme 6 ile Ubuntu 12.10 alfa çalıştırıyorum.

Genel olarak HotSpot birçok indirimi ve basit reduntant operasyonunu optimize eder, bu nedenle genellikle çok fazla seqence yoksa veya çok fazla iç içe olmadıkça bunlar hakkında endişelenmemelisiniz.

Öte yandan, LinkedList için dizinlenmiş get, LinkedList için yineleyicide bir sonraki çağrıdan çok daha yavaştır, bu nedenle yineleyicileri (her döngü için açıkça veya dolaylı olarak) kullanırken okunabilirliği korurken bu performans isabetinden kaçınabilirsiniz.


3

"Get" ifadesinin basit bir dizi araması olduğu bir ArrayList veya Vector gibi bir şeyle bile, ikinci döngü hala birincisinin sahip olmadığı ek yüke sahiptir. İlkinden biraz daha yavaş olmasını beklerdim.


İlk döngü her öğeyi de almak zorundadır. Bunu yapmak için perde arkasında bir yineleyici oluşturur. Gerçekten eşdeğerler.
Bill the Lizard

C açısından düşünüldüğünde, bir yineleyici sadece bir işaretçiyi artırabilir, ancak bir get, i'nin değerini her seferinde bir işaretçinin genişliğiyle çarpmak zorundadır.
Paul Tomblin

Kullandığınız liste türüne bağlıdır. Bence haklısın, get'i kullanmak hiç bu kadar hızlı ve bazen daha yavaş olmaz.
Kertenkele Bill


3

İşte Android geliştirme ekibi tarafından ortaya konulan farkın kısa bir analizi:

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

Sonuç orada olmasıdır olan fark, ve çok büyük listeleri ile çok ölçülü ortamlarda kayda değer bir değişiklik olabilir. Testlerinde, her döngü için iki kat daha uzun sürdü. Bununla birlikte, testleri 400.000 tamsayı içeren bir arraylist üzerinde yapıldı. Dizideki eleman başına gerçek fark 6 mikrosaniyeydi . Test etmedim ve söylemediler, ama farkın ilkellerden ziyade nesneleri kullanarak biraz daha büyük olmasını beklerdim, ama yine de size sorulan şeyin ölçeği hakkında hiçbir fikriniz olmayan bir kütüphane kodu inşa etmedikçe bile yinelemek için, farkın üzerinde durmaya değer olmadığını düşünüyorum.


2

Değişken adına göre objectArrayList, bunun bir örneği olduğunu varsayıyorum java.util.ArrayList. Bu durumda, performans farkı fark edilmeyecektir.

Öte yandan, eğer bir örnekse java.util.LinkedList, ikinci yaklaşım List#get(int)O (n) işlemi olduğu için çok daha yavaş olacaktır .

Dolayısıyla indeks döngüdeki mantık için gerekli olmadıkça ilk yaklaşım her zaman tercih edilir.


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

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

Her ikisi de aynısını yapar, ancak her biri için kolay ve güvenli programlama kullanımı için, 2. kullanım yolunda hataya eğilimli olasılıklar vardır.


1

Hiç kimsenin bariz olandan bahsetmemesi gariptir - foreach bellek (yineleyici şeklinde) ayırır, oysa normal for döngüsü herhangi bir bellek ayırmaz. Android'deki oyunlar için bu bir sorundur, çünkü çöp toplayıcının periyodik olarak çalışacağı anlamına gelir. Bir oyunda çöp toplayıcının çalışmasını istemiyorsunuz ... HİÇ. Bu nedenle, çizim (veya oluşturma) yönteminizde foreach döngülerini kullanmayın.


1

Kabul edilen cevap, ArrayList'in istisnai durumu dışında soruyu cevaplar ...

Çoğu geliştirici ArrayList'e güvendiğinden (en azından buna inanıyorum)

Bu yüzden buraya doğru cevabı eklemek zorundayım.

Doğrudan geliştirici belgelerinden: -

Geliştirilmiş döngü (bazen "her biri için" döngü olarak da bilinir), Yinelenebilir arabirimi uygulayan koleksiyonlar ve diziler için kullanılabilir. Koleksiyonlarda, hasNext () ve next () öğesine arayüz çağrıları yapmak için bir yineleyici ayrılır. Bir ArrayList ile elle yazılmış sayılan döngü yaklaşık 3 kat daha hızlıdır (JIT ile veya JIT olmadan), ancak diğer koleksiyonlar için geliştirilmiş döngü sözdizimi, açık yineleyici kullanımına tam olarak eşdeğer olacaktır.

Bir dizi üzerinden yineleme yapmak için birkaç alternatif vardır:

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 () en yavaştır, çünkü JIT döngü boyunca her yineleme için dizi uzunluğunu alma maliyetini henüz optimize edemez.

one () daha hızlıdır. Aramalardan kaçınarak her şeyi yerel değişkenlere çeker. Yalnızca dizi uzunluğu bir performans avantajı sunar.

two (), bir JIT olmayan cihazlar için en hızlı ve bir () üzerinden JIT olan cihazlar için ayırt edilemez. Java programlama dilinin 1.5 sürümünde tanıtılan gelişmiş döngü sözdizimini kullanır.

Bu nedenle, varsayılan olarak gelişmiş for döngüsünü kullanmalısınız, ancak performans açısından kritik ArrayList yinelemesi için elle yazılmış sayılan bir döngüyü göz önünde bulundurun.


-2

Evet, for-eachvaryant normalden daha hızlı index-based-for-loop.

for-eachvaryant kullanır iterator. Böylece çaprazlama, fordizin tabanlı normal döngüden daha hızlıdır .
Bunun nedeni iterator, geçiş için optimize edilmiş olmasıdır, çünkü bir sonraki öğeden hemen önce ve bir önceki öğeden hemen sonra işaret eder . index-based-for-loopYavaş olmanın nedenlerinden biri, her seferinde olmayan eleman konumunu hesaplamak ve hareket ettirmek zorunda olmasıdır iterator.


-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+" ");
        }
    }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.