Có sự khác biệt về hiệu suất giữa vòng lặp for và vòng lặp for?


184

Điều gì, nếu có, là sự khác biệt hiệu suất giữa hai vòng sau đây?

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

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

@Keparo: Đây là vòng lặp "cho mỗi" không phải là vòng lặp "for-in"
Ande Turner

trong java, nó được gọi là "cho mỗi", nhưng khi đến Objective C, nó được gọi là vòng lặp "for In".
damithH


Phần mở rộng cho hiệu suất vòng lặp được thảo luận ở đây: stackoverflow.com/questions/12155987/
Khăn

Ngay cả khi có một sự khác biệt, nó sẽ rất nhỏ đến mức bạn nên ưu tiên khả năng đọc trừ khi đoạn mã này được thực thi hàng nghìn tỷ lần mỗi giây. Và sau đó bạn sẽ cần một điểm chuẩn thích hợp để tránh tối ưu hóa sớm.
Zabuzard

Câu trả lời:


212

Từ mục 46 trong Java hiệu quả của Joshua Bloch:

Vòng lặp for-Each, được giới thiệu trong phiên bản 1.5, thoát khỏi sự lộn xộn và cơ hội lỗi bằng cách ẩn hoàn toàn biến lặp hoặc biến chỉ số. Thành ngữ kết quả áp dụng như nhau cho các bộ sưu tập và mảng:

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

Khi bạn thấy dấu hai chấm (:), hãy đọc nó dưới dạng. Do đó, vòng lặp ở trên đọc là tên của từng phần tử e trong các phần tử. Lưu ý rằng không có hình phạt về hiệu suất khi sử dụng vòng lặp for-every, ngay cả đối với mảng. Trong thực tế, nó có thể cung cấp một lợi thế hiệu suất nhỏ hơn một vòng lặp thông thường trong một số trường hợp, vì nó chỉ tính giới hạn của chỉ số mảng một lần. Trong khi bạn có thể làm điều này bằng tay (Mục 45), các lập trình viên không phải lúc nào cũng làm như vậy.


48
Đáng nói là trong một vòng lặp cho mỗi vòng lặp không có cách nào để truy cập vào bộ đếm chỉ mục (vì nó không tồn tại)
basszero

Có, nhưng bộ đếm đó hiện có thể nhìn thấy bên ngoài vòng lặp. Chắc chắn, đó là một sửa chữa đơn giản nhưng cũng là cho mỗi!
Mất lòng

74
Có hình phạt hiệu suất của việc phân bổ iterator. Tôi đã có một số mã song song cao trong một hình nền sống Android. Tôi thấy rằng người thu gom rác đang phát điên. Đó là bởi vì mỗi vòng lặp được phân bổ các trình vòng lặp tạm thời trong nhiều luồng khác nhau (thời gian ngắn), khiến cho trình thu gom rác rất nhiều công việc. Chuyển sang các vòng lặp dựa trên chỉ số thông thường đã khắc phục vấn đề.
gsingh2011

1
@ gsingh2011 Nhưng điều này cũng phụ thuộc vào việc bạn có đang sử dụng danh sách truy cập ngẫu nhiên hay không. Sử dụng truy cập dựa trên chỉ mục vào danh sách truy cập không ngẫu nhiên sẽ tệ hơn rất nhiều so với việc sử dụng cho mỗi danh sách truy cập ngẫu nhiên, tôi đoán vậy. Nếu bạn đang làm việc với Danh sách giao diện và do đó không biết loại triển khai thực tế, bạn có thể kiểm tra xem danh sách đó có phải là một phiên bản của RandomAccess hay không, nếu bạn thực sự quan tâm đến điều đó: docs.oracle.com/javase/8 /docs/api/java/util/RandomAccess.html
Puce 18/03/2015

3
@ gsingh2011 Các tài liệu Android ( developer.android.com/training/articles/perf-tips.html#Loops ) đề cập rằng bạn sẽ bị phạt hiệu năng khi chỉ sử dụng foreach trên một ArrayList chứ không phải các bộ sưu tập khác. Tôi tò mò liệu đó có phải là trường hợp của bạn không.
Viccari

29

Tất cả các vòng lặp làm như vậy chính xác, tôi chỉ muốn hiển thị những điều này trước khi ném vào hai xu của tôi.

Đầu tiên, cách lặp cổ điển thông qua Danh sách:

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

Thứ hai, cách ưa thích vì nó ít bị lỗi hơn (BẠN đã thực hiện bao nhiêu lần "oops, trộn lẫn các biến i và j trong các vòng lặp trong vòng lặp"?).

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

Thứ ba, vòng lặp tối ưu hóa vi mô:

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

Bây giờ là hai xu thực tế: Ít nhất là khi tôi đang thử nghiệm những thứ này, thì cái thứ ba là nhanh nhất khi tính mili giây cho mỗi loại vòng lặp với một thao tác đơn giản được lặp lại vài triệu lần - đây là cách sử dụng Java 5 với jre1.6u10 trên Windows trong trường hợp có ai quan tâm.

Mặc dù ít nhất có vẻ như là thứ ba là nhanh nhất, nhưng bạn thực sự nên tự hỏi mình có muốn mạo hiểm thực hiện tối ưu hóa lỗ nhìn trộm này ở mọi nơi trong mã vòng lặp của bạn hay không, từ những gì tôi đã thấy, vòng lặp thực tế không phải là ' Thường là phần tốn thời gian nhất của bất kỳ chương trình thực tế nào (hoặc có thể tôi chỉ làm việc sai lĩnh vực, ai biết được). Và cũng như tôi đã đề cập trong cái cớ cho Java vòng lặp for-every (một số người gọi nó là vòng lặp Iterator và những cái khác là vòng lặp for-in ), bạn sẽ ít gặp phải một lỗi ngu ngốc cụ thể khi sử dụng nó. Và trước khi tranh luận làm thế nào điều này thậm chí có thể nhanh hơn những cái khác, hãy nhớ rằng javac hoàn toàn không tối ưu hóa mã byte (tốt, gần như là vậy), nó chỉ biên dịch nó.

Nếu bạn đang tối ưu hóa vi mô và / hoặc phần mềm của bạn sử dụng nhiều vòng lặp đệ quy và như vậy thì bạn có thể quan tâm đến loại vòng lặp thứ ba. Chỉ cần nhớ điểm chuẩn phần mềm của bạn tốt cả trước và sau khi thay đổi các vòng lặp, bạn phải chuyển sang phần mềm tối ưu hóa vi mô này.


5
Xin lưu ý rằng vòng lặp for có ++ i <= size là "dựa trên 1", ví dụ: phương thức get bên trong vòng lặp sẽ được gọi cho các giá trị 1, 2, 3, v.v.
bóng chuyền

15
Cách tốt hơn để viết vòng lặp tối ưu hóa vi mô là cho (int i = 0, size = String.size (); ++ i <= size;) {} Điều này thích hợp hơn vì nó giảm thiểu phạm vi kích thước
Dónal

1
không phải cái thứ ba bắt đầu từ i = 1 trong lần đầu tiên nó đi qua vòng lặp, bỏ qua phần tử đầu tiên. và nó là một vòng lặp for là không cần thiết. int n = chuỗi.length; while (n -> 0) {System.out.println ("" + n + "" + chuỗi [n]); }
Lassi Kinnunen

1
@ Dónal mà mô hình vòng lặp bỏ lỡ cái đầu tiên và đưa ra một IOOBE. Cái này hoạt động: for (int i = -1, size = list.size (); ++ i <size;)
Nathan Adams

1
"Tất cả các vòng lặp làm chính xác" là không chính xác. Một cái sử dụng truy cập ngẫu nhiên get(int), cái kia sử dụng một Iterator. Hãy xem xét LinkedListhiệu suất của for(int i=0;i<strings.size();i++) { /* do something using strings.get(i) */ }nó kém hơn bao nhiêu vì nó đang thực hiện get(int)n lần.
Steve Kuo

13

Vòng lặp for-every thường được ưu tiên. Phương pháp "lấy" có thể chậm hơn nếu việc triển khai Danh sách bạn đang sử dụng không hỗ trợ truy cập ngẫu nhiên. Ví dụ: nếu LinkedList được sử dụng, bạn sẽ phải chịu một chi phí truyền tải, trong khi cách tiếp cận cho mỗi cách sử dụng một trình vòng lặp theo dõi vị trí của nó trong danh sách. Thông tin thêm về các sắc thái của vòng lặp for-mỗi .

Tôi nghĩ rằng bài viết bây giờ ở đây: vị trí mới

Các liên kết hiển thị ở đây đã chết.


12

Chà, tác động hiệu suất chủ yếu là không đáng kể, nhưng không phải là không. Nếu bạn nhìn vào JavaDoc của RandomAccessgiao diện:

Theo nguyên tắc thông thường, việc triển khai Danh sách sẽ triển khai giao diện này nếu, đối với các trường hợp điển hình của lớp, vòng lặp này:

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

chạy nhanh hơn vòng lặp này:

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

Và cho mỗi vòng lặp đang sử dụng phiên bản với iterator, vì vậy ArrayList, ví dụ, cho mỗi vòng lặp không phải là nhanh nhất.


Thực sự mỗi? Thậm chí một với một mảng? Tôi đọc ở đây stackoverflow.com/questions/1006395/ sắt rằng nó không liên quan đến một trình vòng lặp.
Ondra Žižka

@ OndraŽižka: Vòng lặp for-every sử dụng các vòng lặp khi lặp qua Iterables, nhưng không phải là mảng. Đối với mảng, một vòng lặp for với một biến chỉ số được sử dụng. Có thông tin về điều này trong JLS .
Lii

vâng, vì vậy trước tiên bạn cần tạo nó thành một mảng với toArray, có chi phí.
Lassi Kinnunen

6

Có vẻ như có một sự khác biệt đáng tiếc.

Nếu bạn nhìn vào mã byte được tạo cho cả hai loại vòng lặp, chúng sẽ khác nhau.

Dưới đây là một ví dụ từ mã nguồn Log4j.

Trong /log4j-api/src/main/java/org/apache/logging/log4j/MarkerManager.java, chúng tôi có một lớp bên trong tĩnh được gọi là Log4jMarker định nghĩa:

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

Với vòng lặp tiêu chuẩn:

  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

Với mỗi cái:

  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

Có chuyện gì với THAT Oracle vậy?

Tôi đã thử điều này với Java 7 và 8 trên Windows 7.


7
Đối với những người đang cố gắng đọc phần tháo gỡ, kết quả cuối cùng là mã được tạo bên trong vòng lặp giống hệt nhau, nhưng thiết lập for-every dường như đã tạo thêm một biến tạm thời có chứa tham chiếu đến đối số thứ hai. Nếu biến ẩn bổ sung được đăng ký, nhưng bản thân tham số không trong quá trình tạo mã, thì for-every sẽ nhanh hơn; nếu tham số được ghi trong ví dụ for (;;), thời gian thực hiện sẽ giống hệt nhau. Điểm chuẩn phải không?
Robin Davies

4

Luôn luôn tốt hơn để sử dụng iterator thay vì lập chỉ mục. Điều này là do iterator rất có thể được tối ưu hóa cho việc thực hiện Danh sách trong khi lập chỉ mục (gọi get) có thể không được. Ví dụ LinkedList là một Danh sách nhưng việc lập chỉ mục thông qua các phần tử của nó sẽ chậm hơn so với việc lặp lại bằng cách sử dụng trình vòng lặp.


10
Tôi nghĩ rằng không có thứ gọi là "luôn luôn" trong tối ưu hóa hiệu suất.)
eckes

4

foreach làm cho ý định mã của bạn rõ ràng hơn và thường được ưu tiên hơn một cải tiến tốc độ rất nhỏ - nếu có.

Bất cứ khi nào tôi thấy một vòng lặp được lập chỉ mục, tôi phải phân tích cú pháp lâu hơn một chút để đảm bảo nó thực hiện những gì tôi nghĩ Nó có bắt đầu từ 0 không, có bao gồm hoặc loại trừ điểm kết thúc, v.v.?

Hầu hết thời gian của tôi dường như được dành để đọc mã (mà tôi đã viết hoặc người khác đã viết) và sự rõ ràng hầu như luôn luôn quan trọng hơn hiệu suất. Thật dễ dàng để loại bỏ hiệu suất những ngày này bởi vì Hotspot thực hiện một công việc tuyệt vời như vậy.


4

Các mã sau đây:

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

Cung cấp đầu ra sau trên hệ thống của tôi:

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

Tôi đang chạy Ubuntu 12.10 alpha với bản cập nhật OracleJDK 1.7 6.

Nói chung, HotSpot tối ưu hóa rất nhiều các hoạt động gián tiếp và các thao tác đơn giản, do đó, nói chung, bạn không nên lo lắng về chúng trừ khi có rất nhiều trong số chúng bị mắc kẹt hoặc chúng được lồng rất nhiều.

Mặt khác, get được lập chỉ mục trên LinkedList chậm hơn nhiều so với việc gọi tiếp theo trên iterator cho LinkedList để bạn có thể tránh được hiệu năng đó trong khi vẫn giữ được khả năng đọc khi bạn sử dụng các vòng lặp (rõ ràng hoặc ẩn trong mỗi vòng lặp).


3

Ngay cả với một cái gì đó như ArrayList hoặc Vector, trong đó "get" là một tra cứu mảng đơn giản, vòng lặp thứ hai vẫn có thêm chi phí mà cái đầu tiên không có. Tôi hy vọng nó sẽ chậm hơn một chút so với lần đầu tiên.


Vòng lặp đầu tiên cũng phải có được từng phần tử. Nó tạo ra một iterator đằng sau hậu trường để làm điều này. Chúng thực sự tương đương nhau.
Bill Lizard

Suy nghĩ theo C, một trình vòng lặp chỉ có thể tăng một con trỏ, nhưng một get sẽ phải nhân giá trị của i với chiều rộng của một con trỏ mỗi lần.
Paul Tomblin

Nó phụ thuộc vào loại danh sách bạn sử dụng. Tôi nghĩ rằng bạn đã đúng, sử dụng get sẽ không bao giờ nhanh hơn và đôi khi chậm hơn.
Bill Lizard


3

Dưới đây là một phân tích ngắn gọn về sự khác biệt được đưa ra bởi nhóm phát triển Android:

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

Kết quả là có một sự khác biệt, và trong môi trường rất hạn chế với danh sách rất lớn nó có thể là một sự khác biệt đáng chú ý. Trong thử nghiệm của họ, cho mỗi vòng lặp mất gấp đôi thời gian. Tuy nhiên, thử nghiệm của họ đã vượt qua một danh sách gồm 400.000 số nguyên. Sự khác biệt thực tế cho mỗi phần tử trong mảng là 6 micro giây . Tôi đã không kiểm tra và họ không nói, nhưng tôi hy vọng sự khác biệt sẽ lớn hơn một chút khi sử dụng các đối tượng thay vì nguyên thủy, nhưng thậm chí vẫn trừ khi bạn đang xây dựng mã thư viện nơi bạn không biết quy mô của những gì bạn sẽ được hỏi lặp đi lặp lại, tôi nghĩ rằng sự khác biệt không đáng để nhấn mạnh.


2

Theo tên biến objectArrayList, tôi giả sử đó là một thể hiện củajava.util.ArrayList . Trong trường hợp đó, sự khác biệt hiệu suất sẽ không được chú ý.

Mặt khác, nếu đó là một ví dụ java.util.LinkedList, cách tiếp cận thứ hai sẽ chậm hơn nhiều vì List#get(int)hoạt động O (n).

Vì vậy, cách tiếp cận đầu tiên luôn được ưu tiên trừ khi chỉ số là logic cần thiết trong vòng lặp.


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

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

Cả hai đều giống nhau nhưng để sử dụng lập trình dễ dàng và an toàn cho từng người, có khả năng dễ xảy ra lỗi trong cách sử dụng thứ 2.


1

Thật kỳ lạ khi không ai đề cập đến điều hiển nhiên - foreach phân bổ bộ nhớ (dưới dạng một trình vòng lặp), trong khi một vòng lặp for bình thường không phân bổ bất kỳ bộ nhớ nào. Đối với các trò chơi trên Android, đây là một vấn đề, bởi vì điều đó có nghĩa là trình thu gom rác sẽ chạy định kỳ. Trong trò chơi, bạn không muốn trình thu gom rác chạy ... EVER. Vì vậy, không sử dụng các vòng lặp foreach trong phương thức vẽ (hoặc kết xuất) của bạn.


1

Câu trả lời được chấp nhận trả lời câu hỏi, ngoài trường hợp đặc biệt của ArrayList ...

Vì hầu hết các nhà phát triển đều dựa vào ArrayList (ít nhất tôi cũng tin như vậy)

Vì vậy, tôi có nghĩa vụ phải thêm câu trả lời chính xác ở đây.

Trực tiếp từ tài liệu dành cho nhà phát triển: -

Vòng lặp for nâng cao (đôi khi còn được gọi là vòng lặp "for-every") có thể được sử dụng cho các bộ sưu tập thực hiện giao diện Iterable và cho các mảng. Với các bộ sưu tập, một trình vòng lặp được phân bổ để thực hiện các cuộc gọi giao diện đến hasNext () và next (). Với một ArrayList, một vòng lặp được viết bằng tay nhanh hơn khoảng 3 lần (có hoặc không có JIT), nhưng đối với các bộ sưu tập khác, cú pháp vòng lặp nâng cao sẽ chính xác tương đương với việc sử dụng trình lặp rõ ràng.

Có một số lựa chọn thay thế để lặp qua một mảng:

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 () là chậm nhất, vì JIT chưa thể tối ưu hóa chi phí lấy chiều dài mảng một lần cho mỗi lần lặp qua vòng lặp.

một () là nhanh hơn. Nó kéo mọi thứ ra thành các biến cục bộ, tránh việc tra cứu. Chỉ có chiều dài mảng cung cấp một lợi ích hiệu suất.

hai () là nhanh nhất cho các thiết bị không có JIT và không thể phân biệt với một () cho các thiết bị có JIT. Nó sử dụng cú pháp vòng lặp nâng cao được giới thiệu trong phiên bản 1.5 của ngôn ngữ lập trình Java.

Vì vậy, bạn nên sử dụng vòng lặp for được tăng cường theo mặc định, nhưng hãy xem xét một vòng lặp được tính bằng tay để lặp lại ArrayList quan trọng về hiệu năng.


-2

Có, for-eachbiến thể nhanh hơn bình thường index-based-for-loop.

for-eachbiến thể sử dụng iterator. Vì vậy, di chuyển ngang nhanh hơn forvòng lặp thông thường dựa trên chỉ mục.
Điều này là do iteratorđược tối ưu hóa để duyệt qua, bởi vì nó được trỏ đến ngay trước phần tử tiếp theo và ngay sau phần tử trước đó . Một trong những lý do để bị index-based-for-loopchậm là vì nó phải tính toán và di chuyển đến vị trí phần tử mỗi lần không phải với 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+" ");
        }
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.