Đ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();
}
và
for (int i=0; i<objectArrayList.size(); i++) {
objectArrayList.get(i).DoSomething();
}
Đ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();
}
và
for (int i=0; i<objectArrayList.size(); i++) {
objectArrayList.get(i).DoSomething();
}
Câu trả lời:
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.
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.
get(int)
, cái kia sử dụng một Iterator
. Hãy xem xét LinkedList
hiệ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.
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.
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 RandomAccess
giao 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.
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.
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.
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.
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).
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.
Cách duy nhất để biết chắc chắn là điểm chuẩn nó, và thậm chí điều đó không đơn giản như nó có thể nghe . Trình biên dịch JIT có thể làm những điều rất bất ngờ đối với mã của bạn.
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ó là 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.
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.
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.
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.
Có, for-each
biến thể nhanh hơn bình thường index-based-for-loop
.
for-each
biến thể sử dụng iterator
. Vì vậy, di chuyển ngang nhanh hơn for
vò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-loop
chậ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
.