web-dev-qa-db-ja.com

array.lengthを呼び出すコストはいくらですか

アプリケーションでforループをfor-eachループに更新しているときに、次のような「パターン」がたくさん見つかりました。

for (int i = 0, n = a.length; i < n; i++) {
    ...
}

の代わりに

for (int i = 0; i < a.length; i++) {
    ...
}

ループごとにsize()メソッドを呼び出す必要がないため、コレクションのパフォーマンスが向上することがわかります。しかし、配列では??

それで質問が起こりました:はarray.length通常の変数よりも高価ですか?

49
Stroboskop

いいえ、_array.length_の呼び出しはO(1)または定数時間操作です。

_.length_はpublicfinalarrayメンバーであるため、ローカル変数よりもアクセスに時間がかかりません。 (これは、size()のようなメソッドの呼び出しとは大きく異なります)

最新のJITコンパイラは、とにかく_.length_への呼び出しを最適化する可能性があります。

これを確認するには、OpenJDKのJITコンパイラのソースコードを確認するか、JVMにJITでコンパイルされたネイティブコードをダンプさせてコードを調べます。

JITコンパイラがこれを実行できない場合があることに注意してください。例えば.

  1. 囲みメソッドをデバッグしている場合、または
  2. ループ本体にレジスタスピルを強制するのに十分なローカル変数がある場合。
68
jjnguy

私は昼食に少し時間を過ごしました:

public static void main(String[] args) {
    final int[] a = new int[250000000];
    long t;

    for (int j = 0; j < 10; j++) {
        t = System.currentTimeMillis();
        for (int i = 0, n = a.length; i < n; i++) { int x = a[i]; }
        System.out.println("n = a.length: " + (System.currentTimeMillis() - t));

        t = System.currentTimeMillis();
        for (int i = 0; i < a.length; i++) { int x = a[i]; }
        System.out.println("i < a.length: " + (System.currentTimeMillis() - t));
    }
}

結果:

n = a.length: 672
i < a.length: 516
n = a.length: 640
i < a.length: 516
n = a.length: 656
i < a.length: 516
n = a.length: 656
i < a.length: 516
n = a.length: 640
i < a.length: 532
n = a.length: 640
i < a.length: 531
n = a.length: 641
i < a.length: 516
n = a.length: 656
i < a.length: 531
n = a.length: 656
i < a.length: 516
n = a.length: 656
i < a.length: 516

ノート:

  1. テストを逆にすると、おそらくガベージコレクション(?)が原因で、n = a.lengthi < a.lengthよりも約半分速いことが示されます。
  2. 250000000OutOfMemoryErrorを取得したため、270000000をこれ以上大きくすることはできませんでした。

重要なのは、それは他の誰もが作っているものであり、メモリ不足でJavaを実行する必要がありますが、2つの選択肢の間で速度に大きな違いは見られません。実際に重要なことに関する開発時間。

16
Grant Wagner

大きな違いがあるとは思えません。たとえあったとしても、コンパイル中に最適化されていると思います。そのようなことをマイクロ最適化しようとすると、時間を無駄にしていることになります。最初にコードを読みやすく修正してから、パフォーマンスに問題がある場合はprofilerを使用し、必要に応じてより適切なデータ構造/アルゴリズムを選択することを心配します。then最適化について心配します。プロファイラーが強調表示する部分。

11
IRBMe

配列の長さは、Javaでは配列のメンバー変数(要素と同じではありません)として格納されるため、その長さのフェッチは、通常のクラスからメンバー変数を読み取るのと同じように、定数時間の操作です。 CやC++などの古い言語の多くは、長さを配列の一部として格納しないため、ループが開始する前に長さを格納する必要があります。 Javaでそれを行う必要はありません。

7
Bill the Lizard

その場合は、逆ループを作成してみませんか。

for (int i = a.length - 1; i >= 0; --i) {
    ...
}

ここには2つのマイクロ最適化があります。

  • ループ反転
  • 接尾辞のデクリメント
4
instcode

変数に格納する方が、これまでになく少し速くなる可能性があります。しかし、プロファイラーがそれを問題として指摘した場合、私は非常に驚きます。

バイトコードレベルでは、配列の長さの取得はarraylengthバイトコードを使用して行われます。 iloadバイトコードより遅いかどうかはわかりませんが、気付くほどの違いはないはずです。

3
Michael Myers

この答えはC#の観点からのものですが、Javaにも同じことが当てはまると思います。

C#では、イディオム

for (int i = 0; i < a.length; i++) {    ...}

は配列を反復処理するものとして認識されるため、各配列アクセスではなく、ループ内の配列にアクセスするときに境界チェックが回避されます。

これは、次のようなコードで認識される場合と認識されない場合があります。

for (int i = 0, n = a.length; i < n; i++) {    ...}

または

n = a.length;

for (int i = 0; i < n; i++) {   ...}

この最適化のどれだけがコンパイラーとJITterによって実行されるかはわかりません。特に、JITterによって実行される場合は、3つすべてが同じネイティブコードを生成すると予想されます。

ただし、最初の形式は、間違いなく人々が読みやすいので、それを使用すると思います。

2
Michael Burr

Array.lengthは定数であり、JITコンパイラは両方のインスタンスでそれを確認する必要があります。結果のマシンコードはどちらの場合も同じであると思います。少なくともサーバーコンパイラについては。

1
Chris Vest