次の 2 つのループのパフォーマンスの違いは何ですか?
for (Object o: objectArrayList) {
o.DoSomething();
}
と
for (int i=0; i<objectArrayList.size(); i++) {
objectArrayList.get(i).DoSomething();
}
次の 2 つのループのパフォーマンスの違いは何ですか?
for (Object o: objectArrayList) {
o.DoSomething();
}
と
for (int i=0; i<objectArrayList.size(); i++) {
objectArrayList.get(i).DoSomething();
}
Joshua BlochによるEffective JavaのItem 46から:
リリース 1.5 で導入された for-each ループは、イテレータまたはインデックス変数を完全に非表示にすることで、混乱とエラーの可能性を取り除きます。結果のイディオムは、コレクションと配列に等しく適用されます。
// The preferred idiom for iterating over collections and arrays for (Element e : elements) { doSomething(e); }
コロン (:) がある場合は、「in」と読みます。したがって、上記のループは「要素内の各要素 e に対して」と読みます。配列の場合でも、for-each ループを使用してもパフォーマンスが低下しないことに注意してください。実際、配列インデックスの制限を 1 回だけ計算するため、状況によっては、通常の for ループよりもわずかにパフォーマンスが向上する場合があります。これは手動で行うこともできますが (項目 45)、プログラマーが常にそうするとは限りません。
これらのループはすべてまったく同じことを行います。2 セントを投入する前に、これらを示したいだけです。
まず、リストをループする古典的な方法:
for (int i=0; i < strings.size(); i++) { /* do something using strings.get(i) */ }
第二に、エラーが発生しにくいため、推奨される方法です (「おっと、ループ内のこれらのループで変数 i と j を混在させる」ことを何回行ったことがありますか?)。
for (String s : strings) { /* do something using s */ }
3 番目に、マイクロ最適化された for ループ:
int size = strings.size();
for (int i = -1; ++i < size;) { /* do something using strings.get(i) */ }
実際の 2 セント: 少なくともこれらをテストしていたとき、数百万回繰り返された単純な操作で各タイプのループにかかった時間をミリ秒でカウントすると、3 番目のものが最速でした。これは Java 5 を使用していました。誰かが興味を持っている場合に備えて、Windows で jre1.6u10 を使用します。
少なくとも 3 番目が最速であるように見えますが、ループ コードのあらゆる場所にこのピープホール最適化を実装するリスクを負うかどうかを自問する必要があります。これは通常、実際のプログラムで最も時間がかかる部分です (または、間違った分野で作業しているだけかもしれません)。また、Javaの for-each ループの口実で述べたように ( Iterator ループと呼ぶ人もいれば、 for-in ループと呼ぶ人もいます)、それを使用すると、特定の愚かなバグに遭遇する可能性が低くなります。そして、これがどのように他のものよりも高速であるかを議論する前に、javac はバイトコードをまったく最適化せず (とにかくほとんどまったく)、コンパイルするだけであることを思い出してください。
ただし、マイクロ最適化に興味がある場合、および/またはソフトウェアが多くの再帰ループなどを使用している場合は、3 番目のループ タイプに関心があるかもしれません。for ループをこの奇妙でマイクロ最適化されたものに変更する前後に、ソフトウェアのベンチマークを行うことを忘れないでください。
通常は、for-each ループを使用することをお勧めします。使用している List 実装がランダム アクセスをサポートしていない場合、「get」アプローチは遅くなる可能性があります。たとえば、LinkedList を使用すると、トラバーサル コストが発生しますが、for-each アプローチでは、リスト内の位置を追跡する反復子が使用されます。for-each ループのニュアンスに関する詳細情報。
記事は今ここにあると思います: 新しい場所
ここに表示されているリンクは機能していません。
パフォーマンスへの影響はほとんど重要ではありませんが、ゼロではありません。RandomAccess
インターフェイスのJavaDocを見ると:
経験則として、クラスの典型的なインスタンスで次のループが発生する場合、List 実装はこのインターフェイスを実装する必要があります。
for (int i=0, n=list.size(); i < n; i++) list.get(i);
このループよりも高速に実行されます。
for (Iterator i=list.iterator(); i.hasNext();) i.next();
また、for-each ループはバージョンとイテレータを使用しているためArrayList
、たとえば for-each ループは最速ではありません。
残念ながら違いがあるようです。
両方の種類のループで生成されたバイト コードを見ると、それらは異なっています。
Log4j ソース コードの例を次に示します。
/log4j-api/src/main/java/org/apache/logging/log4j/MarkerManager.java には、以下を定義する Log4jMarker という静的内部クラスがあります。
/*
* 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;
}
標準ループの場合:
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
for-each を使用:
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
あのオラクルはどうしたの?
Windows 7のJava 7および8でこれを試しました。
foreach はコードの意図をより明確にします。これは通常、非常にわずかな速度の向上よりも優先されます (もしあれば)。
索引付けされたループが表示されるたびに、少し長く解析して、思ったとおりに動作することを確認する必要があります。
私の時間のほとんどは、(私が書いた、または他の誰かが書いた) コードを読むことに費やされているようで、ほとんどの場合、パフォーマンスよりも明快さが重要です。Hotspot は非常に優れた機能を備えているため、最近ではパフォーマンスを無視するのは簡単です。
以下は、Android 開発チームが発表した違いの簡単な分析です。
https://www.youtube.com/watch?v=MZOf3pOAM6A
その結果、違いがあり、非常に大きなリストを持つ非常に制限された環境では、顕著な違いになる可能性があります。彼らのテストでは、for each ループに 2 倍の時間がかかりました。ただし、彼らのテストは、400,000 個の整数の配列リストを超えていました。配列内の要素ごとの実際の差は 6マイクロ秒でした。私はテストしておらず、彼らも言いませんでしたが、プリミティブではなくオブジェクトを使用すると、違いがわずかに大きくなると予想されますが、それでも、求められるものの規模がわからないライブラリコードを構築していない限り、繰り返しますが、違いは強調する価値がないと思います。
次のコード:
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);
}
}
私のシステムで次の出力が得られます。
224
-699150247503735895
221
-699150247503735895
220
-699150247503735895
219
-699150247503735895
OracleJDK 1.7 update 6 で Ubuntu 12.10 アルファを実行しています。
一般に、HotSpot は多くの間接操作と単純な冗長操作を最適化するため、一般に、それらが連続して大量に存在するか、重くネストされていない限り、それらについて心配する必要はありません。
一方、LinkedList での indexed get は、LinkedList の iterator で next を呼び出すよりもはるかに遅いため、イテレーターを (for-each ループで明示的または暗黙的に) 使用するときに読みやすさを維持しながら、パフォーマンスの低下を回避できます。
確実に知る唯一の方法は、ベンチマークを実行することですが、それでさえ、思ったほど簡単ではありません。JIT コンパイラは、コードに対して非常に予期しないことを実行できます。
「get」が単純な配列ルックアップである ArrayList や Vector のようなものを使用しても、2 番目のループには、最初のループにはないオーバーヘッドが追加されます。最初よりも少し遅くなると思います。
インデックスを作成する代わりに、イテレータを使用することをお勧めします。これは、イテレータがリストの実装に対して最適化されている可能性が高いのに対し、インデックス付け(getの呼び出し)は最適化されていない可能性があるためです。たとえば、LinkedListはリストですが、その要素を介したインデックス作成は、イテレータを使用した反復処理よりも遅くなります。
ArrayListの例外的なケースを除いて、受け入れられた回答は質問に答えます...
ほとんどの開発者は ArrayList に依存しているため (少なくとも私はそう信じています)
したがって、ここに正しい答えを追加する義務があります。
開発者向けドキュメントから直接:-
強化された for ループ (「for-each」ループとも呼ばれる) は、Iterable インターフェイスを実装するコレクションと配列に使用できます。コレクションでは、hasNext() および next() へのインターフェイス呼び出しを行うためのイテレータが割り当てられます。ArrayList を使用すると、手書きのカウント ループは (JIT の有無にかかわらず) 約 3 倍速くなりますが、他のコレクションでは、強化された for ループ構文は、明示的な反復子の使用とまったく同じになります。
配列を反復処理する方法はいくつかあります。
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() が最も遅いのは、JIT がループの反復ごとに 1 回配列の長さを取得するコストをまだ最適化できていないためです。
one() の方が高速です。ルックアップを回避して、すべてをローカル変数に引き出します。配列の長さだけがパフォーマンス上の利点を提供します。
two() は、JIT を使用しないデバイスで最速であり、JIT を使用するデバイスでは one() と区別がつきません。Java プログラミング言語のバージョン 1.5 で導入された拡張 for ループ構文を使用します。
したがって、デフォルトで強化された for ループを使用する必要がありますが、パフォーマンスが重要な ArrayList の繰り返しには手書きのカウント ループを検討してください。