サーチ…


前書き

このトピックでは、Javaアプリケーションのパフォーマンスに関連するいくつかの「落とし穴」(つまり、初心者のJavaプログラマが間違えている間違い)について説明します。

備考

このトピックでは、非効率的な「マイクロ」Javaコーディングの方法について説明します。ほとんどの場合、非効率性は比較的低いですが、それを避けることはまだ可能です。

落とし穴 - ログメッセージを作成するオーバーヘッド

TRACEDEBUGログレベルは、実行時に与えられたコードの操作について高い詳細を伝えることができます。ログレベルをこれらのレベルより上に設定することが通常推奨されますが、見た目は「オフ」になってもパフォーマンスに影響を与えないようにこれらのステートメントに注意する必要があります。

このログステートメントを考えてみましょう:

// Processing a request of some kind, logging the parameters
LOG.debug("Request coming from " + myInetAddress.toString() 
          + " parameters: " + Arrays.toString(veryLongParamArray));

ログレベルがINFOに設定されていても、 debug()渡された引数は、行が実行されるたびに評価されます。これにより、いくつかの計算で不必要に消費されます。

  • String連結:複数のStringインスタンスが作成されます
  • InetAddressはDNSルックアップを行う場合もあります。
  • veryLongParamArrayは非常に長いかもしれません - それからStringを作成するとメモリを消費し、時間がかかります

溶液

ほとんどのロギング・フレームワークは、修正文字列とオブジェクト参照を使用してログ・メッセージを作成する手段を提供します。ログメッセージは、メッセージが実際にログに記録された場合にのみ評価されます。例:

// No toString() evaluation, no string concatenation if debug is disabled
LOG.debug("Request coming from {} parameters: {}", myInetAddress, parameters));

これは、 String.valueOf(Object)を使用してすべてのパラメータを文字列に変換できる限り、うまく動作します。ログメッセージcompuationがより複雑な場合、ログレベルをログに記録する前にチェックすることができます。

if (LOG.isDebugEnabled()) {
    // Argument expression evaluated only when DEBUG is enabled
    LOG.debug("Request coming from {}, parameters: {}", myInetAddress,
              Arrays.toString(veryLongParamArray);
}

ここでは、コストのかかるArrays.toString(Obect[])計算をLOG.debug()は、 DEBUGが実際に有効になっているときにのみ処理されます。

落とし穴 - ループ内の文字列の連結は拡大縮小されません

次のコードを例として考えてみましょう。

public String joinWords(List<String> words) {
    String message = "";
    for (String word : words) {
        message = message + " " + word;
    }
    return message;
}

残念ながら、このコードはwordsリストが長いと非効率です。問題の根源は次のとおりです。

message = message + " " + word;

各ループ反復で、このステートメントは、元のmessage文字列内のすべての文字のコピーを含む新しいmessage文字列を作成し、余分な文字が追加されます。これは一時的な文字列をたくさん生成し、多くのコピーを行います。

joinWordsを分析すると、平均長さがMのN個の単語があると仮定すると、O(N)個の一時文字列が作成され、O(MN 2 )文字がそのプロセスでコピーされることがわかります。 N 2成分は特に問題となる。

この種の問題1の推奨されるアプローチは、次のように文字列連結の代わりにStringBuilderを使用することです。

public String joinWords2(List<String> words) {
    StringBuilder message = new StringBuilder();
    for (String word : words) {
        message.append(" ").append(word);
    }
    return message.toString();
}

joinWords2の分析では、Builderの文字を保持するStringBuilderバッキング配列を「 joinWords2 」するオーバーヘッドを考慮する必要があります。しかし、作成される新しいオブジェクトの数はO(logN)であり、コピーされる文字数はO(MN)文字であることが分かります。後者は、最後のtoString()呼び出しでコピーされた文字を含みます。

(これをさらに調整するには、適切な容量のStringBuilderを作成することがありますが、全体的な複雑さは変わりません)。

元のjoinWordsメソッドに戻ると、クリティカルなステートメントは典型的なJavaコンパイラによって次のように最適化されます。

  StringBuilder tmp = new StringBuilder();
  tmp.append(message).append(" ").append(word);
  message = tmp.toString();

しかし、JavaコンパイラはjoinWords2のコードで手作業で行ったように、 StringBuilderループから「 joinWords2ない」わけではありません。

参照:


1 - Java 8以降では、この特定の問題を解決するために、 Joinerクラスを使用できます。しかし、それはこの例が本当に想定しているものではありません。

落とし穴 - 'new'を使ってプリミティブラッパーインスタンスを作成するのは非効率的です

Java言語では、 IntegerBooleanなどのインスタンスを作成するためにnewを使用できますが、一般的には悪い考えです。オートボクシング(Java 5以降)またはvalueOfメソッドを使用する方が良いです。

 Integer i1 = new Integer(1);      // BAD
 Integer i2 = 2;                   // BEST (autoboxing)
 Integer i3 = Integer.valueOf(3);  // OK

new Integer(int)明示的に使用するのは、(JITコンパイラによって最適化されていない限り)新しいオブジェクトを作成するということです。対照的に、autoboxingまたは明示的なvalueOf呼び出しが使用されると、Javaランタイムは、既存のオブジェクトのキャッシュからIntegerオブジェクトを再利用しようとします。実行時にキャッシュ "ヒット"が発生するたびに、オブジェクトの作成が回避されます。これはまたヒープメモリを節約し、オブジェクトの乱れに起因するGCオーバーヘッドを削減します。

ノート:

  1. 最近のJavaの実装では、autoboxingはvalueOfを呼び出すことで実装され、 BooleanByteShortIntegerLongCharacterキャッシュがあります。
  2. 整数型のキャッシング動作は、Java言語仕様で規定されています。

落とし穴 - 新しい文字列(String)を呼び出すことは非効率的です

文字列を複製するためにnew String(String)を使用することは非効率的でほとんど常に不要です。

  • Stringオブジェクトは不変なので、変更を防ぐためにコピーする必要はありません。
  • 古いバージョンのJavaでは、 Stringオブジェクトは、他のStringオブジェクトとバッキング配列を共有できます。これらのバージョンでは、(大きい)文字列の(小さな)部分文字列を作成し、それを保持することによってメモリをリークすることが可能です。しかし、Java 7以降では、 Stringバッキング配列は共有されません。

具体的なメリットがない場合、 new String(String)呼び出すことは単に無駄です。

  • コピーを作成するにはCPU時間がかかります。
  • このコピーは、メモリの使用量が増え、アプリケーションのメモ帳が増えたり、GCオーバーヘッドが増加したりします。
  • Stringオブジェクトをコピーすると、 equals(Object)hashCode() equals(Object)などの操作が遅くなる可能性があります。

落とし穴 - System.gc()の呼び出しは非効率的です

System.gc()を呼び出すのは(ほとんどの場合)悪い考えです。

gc()メソッドのjavadocは、次を指定します。

「コールgcメソッドは、メソッド呼び出しから制御が戻ると、Java仮想マシンが再利用する最善の努力をしたとき。Java仮想マシンは、彼らが現在のメモリーをすばやく再利用可能なために使用されていないオブジェクトをリサイクルことを示唆していますすべての破棄されたオブジェクトからのスペース。

これから取り上げることができる重要な点がいくつかあります:

  1. 「教えてください」ではなく「示唆」という言葉を使用するということは、JVMがその提案を無視することが自由であることを意味します。デフォルトのJVMの動作(最近のリリース)は、この提案に従うことですが、JVMを起動するときに-XX:+DisableExplicitGC設定することでオーバーライドできます。

  2. 「すべての破棄されたオブジェクトから空間を再利用するための最善の努力」というフレーズは、 gcを呼び出すと「完全な」ガベージコレクションが開始されることを意味します。

だから、なぜSystem.gc()を悪い考え方と呼びますか?

まず、完全なガベージコレクションを実行するのは高価です。完全なGCには、到達可能なすべてのオブジェクトを訪問して「マークする」ことが含まれます。つまり、ゴミではないすべてのオブジェクトです。収集するガベージがあまりないときにこれをトリガーすると、GCはほとんど効果がなく、多くの作業を行います。

第2に、完全なガベージコレクションは、収集されないオブジェクトの「局所性」特性を妨害する傾向がある。ほぼ同じ時間に同じスレッドによって割り当てられたオブジェクトは、メモリ内で密接に割り当てられる傾向があります。これはいい。同時に割り当てられたオブジェクトは関連している可能性があります。すなわち互いに参照する。アプリケーションでこれらの参照を使用している場合は、さまざまなメモリとページキャッシング効果のためにメモリアクセスが高速になる可能性があります。残念ながら、完全なガベージコレクションはオブジェクトを移動させる傾向があり、その結果、一度閉じることができたオブジェクトはさらに離れてしまいます。

第3に、完全なガベージコレクションを実行すると、コレクションが完了するまでアプリケーションが一時停止する可能性があります。これが起こっている間、あなたのアプリケーションは応答しません。

実際、最良の戦略は、JVMがGCをいつ実行するか、実行するコレクションの種類を決定させることです。干渉しない場合、JVMはスループットを最適化するか、GCの一時停止時間を最小限にする時間と収集タイプを選択します。


最初は "...(ほとんどいつでも)悪い考え..."と言った。実際には、それ良いアイデアかもしれないいくつかのシナリオがあります:

  1. ガベージコレクションに敏感なコード(例えばファイナライザや弱い/柔らかい/ファントム参照を含むもの)の単体テストを実装する場合は、 System.gc()呼び出す必要があります。

  2. いくつかのインタラクティブなアプリケーションでは、ガベージコレクションの一時停止があるかどうかをユーザーが気にしない特定の時点が存在する可能性があります。 1つの例は、「遊び」に自然な休止があるゲームである。新しいレベルを読み込むときなどに使用します。

落とし穴 - プリミティブラッパータイプの過剰使用は非効率的です

次の2つのコードを考えてみましょう。

int a = 1000;
int b = a + 1;

そして

Integer a = 1000;
Integer b = a + 1;

質問:どのバージョンが効率的ですか?

回答:2つのバージョンはほぼ同じですが、最初のバージョンは2つ目のバージョンよりもずっと効率的です。

2番目のバージョンは、より多くのスペースを使用する数字の表現を使用しており、自動ボクシングと裏の自動アンボクシングに頼っています。実際、2番目のバージョンは次のコードに直接相当します。

Integer a = Integer.valueOf(1000);               // box 1000
Integer b = Integer.valueOf(a.intValue() + 1);   // unbox 1000, add 1, box 1001

これをintを使用する他のバージョンと比較すると、 Integerが使用されているときに3つの余分なメソッド呼び出しがあることは明らかです。 valueOfの場合、呼び出しはそれぞれ新しいIntegerオブジェクトを作成して初期化します。この余分なボクシングとアンボクシング作業のすべては、第2のバージョンを最初のものよりも1桁遅くする可能性が高い。

それに加えて、第2のバージョンは、各valueOf呼び出しでヒープ上にオブジェクトを割り当てることです。スペース使用率はプラットフォーム固有ですが、各Integerオブジェクトの16バイトの領域にある可能性があります。対照的に、 intバージョンは、 bbがローカル変数でaと仮定してa余分なヒープスペースをゼロにする必要があります。


プリミティブがより高速である別の大きな理由は、ボックス化された等価物が、それぞれの配列タイプがどのようにメモリにレイアウトされるかということです。

int[]Integer[]を例にとると、 int[]場合、 int はメモリ内で連続して配置されます。しかし、 Integer[]場合、配置される値ではなく、実際のint値を含むIntegerオブジェクトへの参照(ポインタ)です。

余分なレベルの間接指定に加えて、これは値を反復処理するときにキャッシュの局所性になると、大きなタンクになります。 int[]の場合、CPUはメモリ内で連続しているため、配列のすべての値を一度にキャッシュに取り込むことができます。しかし、 Integer[]の場合、配列には実際の値への参照しか含まれていないので、CPUは各要素に対して追加のメモリフェッチを実行する必要があります。


要するに、プリミティブラッパー型の使用は、CPUリソースとメモリリソースの両方で比較的高価です。それらを不必要に使用することは効率的です。

落とし穴 - マップのキーを反復すると効率が悪くなることがある

次のコード例は、必要な速度よりも遅いです。

Map<String, String> map = new HashMap<>(); 
for (String key : map.keySet()) {
    String value = map.get(key);
    // Do something with key and value
}

これは、マップ内の各キーに対してマップルックアップ( get()メソッド)が必要なためです。このルックアップは効率的ではないかもしれません(HashMapでは、キーにhashCodeを呼び出し、内部データ構造内の正しいバケットを検索し、場合によってはequals呼び出すこともあります)。大規模な地図では、これは簡単なオーバーヘッドではないかもしれません。

これを回避する正しい方法は、マップのエントリを繰り返し処理することです( コレクションのトピックで詳しく説明しています)。

落とし穴 - コレクションが空であるかどうかを調べるためにsize()を使うことは非効率的です。

Java Collections Frameworkは、すべてのCollectionオブジェクトに関連する2つのメソッドを提供します。

  • size()は、 Collection内のエントリの数を返します。
  • isEmpty()メソッドは、 Collectionが空の場合にのみtrueを返します。

両方のメソッドを使用して、コレクションの空白をテストできます。例えば:

Collection<String> strings = new ArrayList<>();
boolean isEmpty_wrong = strings.size() == 0; // Avoid this
boolean isEmpty = strings.isEmpty();         // Best

これらのアプローチは同じように見えますが、コレクション実装の中にはサイズを格納しないものがあります。そのようなコレクションの場合、 size()実装は呼び出されるたびにサイズを計算する必要があります。例えば:

  • 単純なリンクリストクラス( java.util.LinkedListではなく)は、要素を数えるためにリストをトラバースする必要があります。
  • ConcurrentHashMapクラスは、マップのすべての「セグメント」内のエントリを合計する必要があります。
  • 怠惰なコレクションの実装では、要素を数えるためにコレクション全体をメモリ内で実現する必要があります。

対照的に、 isEmpty()メソッドは、コレクション内に少なくとも1つの要素があるかどうかをテストするだけです。これは要素を数えることを必要としない。

しながら、 size() == 0常にことあまり効率的ではないisEmpty() 、それが適切に実施するために考えられないisEmpty()未満で効率的にsize() == 0 。したがって、 isEmpty()が優先されます。

落とし穴 - 正規表現による効率の問題

正規表現のマッチングは強力なツール(Javaやその他のコンテキストで)ですが、いくつかの欠点があります。これらのうちの1つは、正規表現がかなり高価になる傾向があります。

PatternインスタンスとMatcherインスタンスを再利用する必要があります

次の例を考えてみましょう。

/**
 * Test if all strings in a list consist of English letters and numbers.
 * @param strings the list to be checked
 * @return 'true' if an only if all strings satisfy the criteria
 * @throws NullPointerException if 'strings' is 'null' or a 'null' element.
 */
public boolean allAlphanumeric(List<String> strings) {
    for (String s : strings) {
        if (!s.matches("[A-Za-z0-9]*")) {
            return false;
        }  
    }
    return true;
}

このコードは正しいが、非効率的である。問題は、 matches(...)呼び出しにあります。フードの下では、 s.matches("[A-Za-z0-9]*")は次のようになります。

Pattern.matches(s, "[A-Za-z0-9]*")

これは次に

Pattern.compile("[A-Za-z0-9]*").matcher(s).matches()

Pattern.compile("[A-Za-z0-9]*")呼び出しは、正規表現を解析し、解析し、正規表現エンジンで使用されるデータ構造を保持するPatternオブジェクトを構築します。これは単純な計算ではありません。次に、引数sをラップするためにMatcherオブジェクトが作成されます。最後にmatch()を呼び出して実際のパターンマッチングを行います。

問題は、この作業がループの繰り返しごとに繰り返されることです。ソリューションは、次のようにコードを再構成することです。

private static Pattern ALPHA_NUMERIC = Pattern.compile("[A-Za-z0-9]*");

public boolean allAlphanumeric(List<String> strings) {
    Matcher matcher = ALPHA_NUMERIC.matcher("");
    for (String s : strings) {
        matcher.reset(s);
        if (!matcher.matches()) {
            return false;
        }  
    }
    return true;
}

Patternjavadocは次のような状態になることに注意してください。

このクラスのインスタンスは不変であり、複数の同時スレッドで安全に使用できます。 Matcherクラスのインスタンスは、このような使用には安全ではありません。

find()を使うべきときはmatch()を使わないでください。

文字列sに3桁以上の数字が含まれているかどうかをテストしたいとします。あなたは、以下を含む様々な方法でこれを表現します:

  if (s.matches(".*[0-9]{3}.*")) {
      System.out.println("matches");
  }

または

  if (Pattern.compile("[0-9]{3}").matcher(s).find()) {
      System.out.println("matches");
  }

最初の方が簡潔ですが、効率が悪くなる可能性もあります。それに直面して、最初のバージョンでは文字列全体をパターンと照合しようとしています。さらに、 "。*"は "greedy"パターンなので、パターンマッチャーは文字列の最後まで "熱心に"進んで、マッチするまでバックトラックします。

対照的に、第2のバージョンは左から右へ検索し、3桁の行を見つけるとすぐに検索を停止します。

より効率的な正規表現の代替を使用する

正規表現は強力なツールですが、あなたの唯一のツールではありません。多くのタスクは、他の方法でより効率的に実行できます。例えば:

 Pattern.compile("ABC").matcher(s).find()

同じことをします:

 s.contains("ABC")

後者はずっと効率的です。 (たとえ正規表現をコンパイルするコストを償却することができます)。

しばしば、非正規表現形式はより複雑です。たとえば、 matches()メソッドが実行したテストは、以前のallAlplanumericメソッドを次のように書き直すことができます。

 public boolean matches(String s) {
     for (char c : s) {
         if ((c >= 'A' && c <= 'Z') ||
             (c >= 'a' && c <= 'z') ||
             (c >= '0' && c <= '9')) {
              return false;
         }
     }
     return true;
 }

Matcherを使用するよりコードが多くなりましたが、これもかなり高速になります。

悲惨なバックトラッキング

(これは、正規表現のすべての実装で問題になる可能性がありますが、 Pattern使用の落とし穴であるためここで言及します)。

この(考察された)例を考えてみましょう:

Pattern pat = Pattern.compile("(A+)+B");
System.out.println(pat.matcher("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB").matches());
System.out.println(pat.matcher("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC").matches());

最初のprintln呼び出しはすぐにtrue 。 2番目のものはfalse 。最終的に。実際に、上記のコードを試してみると、 C A前にAを追加するたびに時間が2倍になることがわかります。

これは悲劇的なバックトラッキングの例です。正規表現マッチングを実装するパターンマッチングエンジンは、パターン一致する可能性のあるすべての方法を無駄なく試しています。

(A+)+B実際に意味するものを見てみましょう。表面的には、「1つ以上のA文字とそれに続くB値」と言われていますが、実際にはそれぞれが1つ以上のA文字で構成される1つ以上のグループがあります。したがって、たとえば:

  • 'AB'は一方通行にのみ一致します: '(A)B'
  • 「AAB」は、「(AA)B」または「(A)(A)B」という2つの方法に一致します。
  • (AAA)Bまたは(AA)(A)B or '(A)(AA)Bまたは(A)(A)(A)Bの4つの方法に一致する。
  • 等々

換言すれば、可能な一致の数は2 Nであり、NはA文字の数でA

上記の例ははっきりと考案されていますが、このようなパフォーマンス特性(つまり、大きなKに対してO(2^N)またはO(N^K)を示すパターンは、あまり考慮されていない正規表現を使用すると頻繁に発生します。いくつかの標準的な救済策があります:

  • 他の繰り返しパターン内に繰り返しパターンを入れ子にしないでください。
  • あまりにも多くの繰り返しパターンを使用しないでください。
  • 必要に応じて、バックトラッキングのないリピートを使用します。
  • 複雑な解析作業には正規表現を使用しないでください。 (代わりに適切なパーサーを作成してください。)

最後に、ユーザーまたはAPIクライアントが病理学的特性を持つ正規表現文字列を提供できる状況に注意してください。それは、偶発的または意図的な「サービス拒否」につながる可能性があります。

参考文献:

落とし穴 - ==を使うことができるようにするためのインターン文字列は悪い考えです

一部のプログラマーがこのアドバイスを見ると、

" ==を使用した文字列のテストは不正です(文字列が中断されていない限り)"

彼らの最初の反応は、 ==を使うことができるように、文字列を使用することです。 (結局、 ==String.equals(...)呼び出すよりも高速String.equals(...) 、そうではありません)。

これは、さまざまな観点から、間違ったアプローチです。

脆弱性

まず、テストしているすべてStringオブジェクトがインターンされていることを知っていれば、 ==安全に使うことができます。 JLSは、ソースコード中の文字列リテラルがインターンされることを保証します。しかし、標準のJava SE APIのどれも、 String.intern(String)とは別に、 String.intern(String)文字列を返すことを保証していません。インターンされていないStringオブジェクトのソースが1つしか見つからない場合、アプリケーションは信頼できません。その信頼できないことは、それを検出することをより困難にする可能性がある例外ではなく、偽陰性として現れる。

'intern()'を使用するコスト

内部では、以前に中断されたStringオブジェクトを含むハッシュテーブルをメンテナンスすることによって動作します。内部ハッシュテーブルがストレージリークにならないように、何らかの弱参照メカニズムが使用されます。ハッシュテーブルはネイティブコードで実装されていますが( HashMapHashTableなどとは異なります)、 internコールは使用されるCPUとメモリに関して比較的コストがかかります。

このコストは、 equals代わりに==を使用することで得られる節約と比較する必要equalsます。実際には、各拘束された文字列が他の文字列と「数回」比較されなければ、壊れません。

(別名:インターンシップが価値のあるいくつかの状況では、同じ文字列が何回も繰り返されるアプリケーションのメモリフットプリントを減らす傾向がありこれらの文字列の寿命は長くなります)。

ガベージコレクションへの影響

上記のCPUとメモリの直接コストに加えて、内部文字列はガベージコレクタのパフォーマンスに影響します。

Java 7より前のバージョンのJavaの場合、保留された文字列は、まれに収集される「PermGen」スペースに保持されます。 PermGenを収集する必要がある場合、これは(通常は)完全なガベージコレクションをトリガーします。 PermGenスペースが完全に埋まると、通常のヒープスペースに空き領域があってもJVMがクラッシュします。

Java 7では、文字列プールが "PermGen"から通常のヒープに移動されました。ただし、ハッシュテーブルは引き続き長期間使用されるデータ構造になり、これによって内部の文字列が長寿命になります。 (たとえ拘束された文字列オブジェクトがEden空間に割り当てられたとしても、それらは収集される前にプロモートされる可能性が最も高いでしょう)。

したがって、すべての場合において、文字列をインターンにすることは、通常の文字列に比べて文字列の存続期間を延ばすことになります。これにより、JVMの存続期間にわたってガベージコレクションのオーバーヘッドが増加します。

第2の問題は、ハッシュテーブルが文字列の内部メモリ漏洩を防ぐために、ある種の弱い参照メカニズムを使用する必要があることです。しかし、そのような仕組みはガベージコレクタのためのより多くの仕事です。

これらのガベージコレクションのオーバーヘッドは定量化するのが難しいですが、存在することはほとんどありません。あなたが広範にinternを使用する場合、それらは重要な可能性があります。

文字列プールハッシュテーブルサイズ

このソースによれば、Java 6以降では、文字列プールは、同じバケットにハッシュする文字列を扱うチェーン付きの固定サイズのハッシュテーブルとして実装されています。 Java 6の初期リリースでは、ハッシュテーブルは(ハード配線された)一定のサイズを持っていました。チューニング・パラメータ( -XX:StringTableSize )がJava 6の中期更新版として追加されました。その後、Java 7の中期更新版では、プールのデフォルト・サイズが1009から60013に変更されました。

一番下の行は、あなたが使用するつもりならばということでinternあなたのコードに集中、ハッシュテーブルのサイズが調整可能であるところのJavaのバージョンを選択し、適切にそれをチューニング大きさ、それを確認することをお勧めします。そうしないと、プールが大きくなるにつれて、 internのパフォーマンスが低下する傾向があります。

潜在的なサービス拒否のベクターとしてのインターン

文字列のハッシュコードアルゴリズムはよく知られています。悪意のあるユーザーやアプリケーションから提供された文字列を使用すると、DoS(Denial of Service)攻撃の一部として使用される可能性があります。悪意のあるエージェントが提供するすべての文字列が同じハッシュコードを持っているとすると、アンバランスなハッシュテーブルとintern O(N)パフォーマンスにつながります... Nは衝突した文字列の数です。

DoS攻撃の目的がセキュリティを破ることである場合や、DoS防御の第一線を回避する場合は、このベクトルを使用することができます。

落とし穴 - バッファされていないストリームに対する小さな読み込み/書き込みは非効率的です

あるファイルを別のファイルにコピーするには、次のコードを検討してください。

import java.io.*;

public class FileCopy {

    public static void main(String[] args) throws Exception {
        try (InputStream is = new FileInputStream(args[0]);
             OutputStream os = new FileOutputStream(args[1])) {
           int octet;
           while ((octet = is.read()) != -1) {
               os.write(octet);
           }
        }
    }
}

(この例のポイントには関係しないので、通常の引数チェックやエラー報告などを省略して審議しています。)

上記のコードをコンパイルして巨大なファイルをコピーする場合、非常に遅いことがわかります。実際には、標準のOSファイルコピーユーティリティよりも数桁も遅くなるでしょう。

実際のパフォーマンス測定値をここに追加してください!

上記の例が遅い(大きなファイルの場合)主な理由は、バッファされていないバイトストリームに対して1バイトの読み取りと1バイトの書き込みを実行していることです。パフォーマンスを向上させる簡単な方法は、ストリームをバッファリングされたストリームでラップすることです。例えば:

import java.io.*;

public class FileCopy {

    public static void main(String[] args) throws Exception {
        try (InputStream is = new BufferedInputStream(
                     new FileInputStream(args[0]));
             OutputStream os = new BufferedOutputStream(
                     new FileOutputStream(args[1]))) {
           int octet;
           while ((octet = is.read()) != -1) {
               os.write(octet);
           }
        }
    }
}

これらの小さな変更は、プラットフォームのさまざまな要因に応じて、データのコピー速度を少なくとも数桁も改善します。バッファリングされたストリームラッパーによって、データはより大きなチャンクで読み書きされます。インスタンスには、両方ともバイト配列として実装されたバッファがあります。

  • is 、データは一度バッファにファイルから数キロバイトを読まれます。 read()が呼び出されると、実装は通常、バッファからバイトを返します。バッファが空になっている場合は、基本となる入力ストリームからのみ読み込みます。

  • osの動作は類似しています。 os.write(int)呼び出すと、バッファに1バイトが書き込まれます。データは、バッファが一杯になったとき、またはosがフラッシュされたとき、または閉じられたときにのみ、出力ストリームに書き込まれます。

文字ベースのストリームはどうですか?

Java I / Oは、バイナリデータとテキストデータの読み書き用に異なるAPIを提供しています。

  • InputStreamOutputStreamはストリームベースのバイナリI / Oの基本APIです
  • ReaderWriterはストリームベースのテキストI / Oの基本APIです。

テキストI / Oについては、 BufferedReaderBufferedWriterための等価物であるBufferedInputStreamBufferedOutputStream

なぜバッファリングされたストリームがこれを大きく変えるのでしょうか?

バッファリングされたストリームがパフォーマンスを向上させる本当の理由は、アプリケーションがオペレーティングシステムと通信する方法と関係することです。

  • JavaアプリケーションのJavaメソッド、またはJVMのネイティブ・ランタイム・ライブラリーのネイティブ・プロシージャー呼び出しは高速です。典型的には、いくつかの機械命令を取り、パフォーマンスの影響はほとんどありません。

  • 対照的に、オペレーティングシステムへのJVMランタイムコールは高速ではありません。それらには「システムコール」と呼ばれるものが含まれます。システムコールの典型的なパターンは次のとおりです。

    1. syscall引数をレジスタに入れます。
    2. SYSENTERトラップ命令を実行します。
    3. トラップハンドラは特権状態に切り替え、仮想メモリのマッピングを変更します。次に、特定のシステムコールを処理するコードにディスパッチします。
    4. syscallハンドラは、ユーザプロセスが参照してはならないメモリにアクセスするように指示されていないことに注意して、引数をチェックします。
    5. システムコール固有の作業が実行されます。 readシステムの場合、これには以下が含まれます:
      1. ファイルディスクリプタの現在位置で読み込むデータがあるかどうかをチェックする
      2. ファイルシステムハンドラを呼び出して必要なデータをディスク(または格納されている場所)からバッファキャッシュにフェッチし、
      3. バッファ・キャッシュからJVM提供のアドレスにデータをコピーする
      4. thstreamポインタファイル記述子の位置を調整する
    6. システムコールから戻るこれには、VMマッピングの変更と特権状態からの切り替えが必要です。

あなたが想像しているように、1つのシステムコールを実行すると何千もの機械命令が実行されます。保守的には、通常のメソッド呼び出しよりも少なくとも 2桁長い。 (おそらく3つ以上。)

このことを考えると、バッファリングされたストリームが大きな違いを生むのは、それらがシステムコールの数を大幅に減らすからです。 read()呼び出しごとにsyscallを実行する代わりに、バッファされた入力ストリームは必要に応じて大量のデータをバッファに読み込みます。バッファリングされたストリーム上のほとんどのread()呼び出しは、以前に読み込まれたbyteをチェックして返しbyte 。同様の推論が出力ストリームの場合、および文字ストリームの場合にも当てはまります。

(バッファリングされたI / Oパフォーマンスは、読み取り要求サイズとディスクブロックのサイズ、ディスク回転待ち時間などの不一致から来ていると考えている人もいます。アプリケーションは通常 、ディスクを待つ必要はありませんが、実際の説明ではありません。)

バッファリングされたストリームは常に勝つのですか?

常にではない。バッファリングされたストリームは、あなたのアプリケーションがたくさんの "小さな"読み込みや書き込みを行う場合、間違いなく勝利です。しかし、アプリケーションが大きなbyte[]またはchar[]との読み書きを実行する必要がある場合、バッファされたストリームはあなたに本当の利点を与えません。実際、(小さな)パフォーマンス上のペナルティさえあるかもしれません。

これはJavaでファイルをコピーする最も速い方法ですか?

いいえ、そうではありません。 JavaのストリームベースのAPIを使用してファイルをコピーすると、データのメモリからメモリへの余分なコピーが1つ以上必要になります。 NIO ByteBufferChannel APIを使用する場合は、これを避けることができます。 ( 別の例へのリンクをここに追加してください。



Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow