例外のスローのどの部分が高価ですか?


256

Javaでは、実際にはエラーがない場合にロジックの一部としてthrow / catchを使用することは、(一部では)一般に悪い考えです。例外のスローとキャッチはコストがかかり、ループで何度も実行することは通常、他の方法よりもはるかに遅いためです。例外のスローを含まない制御構造。

私の質問は、スロー/キャッチ自体で発生するコストですか、それとも例外オブジェクトを作成するときに発生しますか(実行スタックを含む多くのランタイム情報を取得するため)?

つまり、もし

Exception e = new Exception();

しかしそれを投げないでください、それは投げのコストのほとんどですか、それとも投げ+キャッチは高価なものを処理していますか?

コードをtry / catchブロックに配置すると、そのコードを実行するコストが増えるかどうかは尋ねていません。例外のキャッチが高価な部分であるか、または例外の作成(コンストラクターの呼び出し)が高価な部分であるかどうかを尋ねています。 。

これを尋ねる別の方法は、例外のインスタンスを1つ作成し、それを何度もスローしてキャッチした場合、スローするたびに新しい例外を作成するよりもはるかに速いでしょうか?


20
スタックトレースの入力と入力が行われていると思います。
Elliott Frisch 2016年


「例外のインスタンスを1つ作成し、それを何度も投げてキャッチした場合」、例外が作成されると、スタックトレースが満たされます。つまり、スローされた場所に関係なく、常に同じstactraceになります。スタックトレースがあなたにとって重要ではない場合、あなたはあなたの考えを試すことができますが、これは場合によっては不可能ではないにしても、非常に困難なデバッグになる可能性があります。
Pshemo 2016年

2
@Pshemo 実際にコードでこれを実行する予定はありません。パフォーマンスについて質問し、この不条理を、違いをもたらす可能性のある例として使用します。
Martin Carney

@MartinCarney最後の段落に回答を追加しました。つまり、例外をキャッシュするとパフォーマンスが向上します。便利な場合はコードを追加でき、そうでない場合は回答を削除できます。
ハリー

回答:


267

例外オブジェクトの作成は、他の通常のオブジェクトの作成よりもコストがかかりません。主なコストはネイティブに隠されていますfillInStackTrace、コールスタックを調べ、スタックトレースを構築するために必要なすべての情報(クラス、メソッド名、行番号など)を収集メソッドにます。

例外コストが高いという神話は、ほとんどのThrowableコンストラクタが暗黙的にを呼び出すという事実に由来していますfillInStackTrace。ただし、スタックトレースなしでを作成するコンストラクターが1つThrowableあります。これにより、インスタンス化が非常に高速なスロー可能オブジェクトを作成できます。軽量の例外を作成する別の方法は、オーバーライドすることfillInStackTraceです。


今、何についての投げる例外を?
実際には、スローされた例外がどこでキャッチされるかによって異なりますます。

同じメソッドでキャッチされた場合(より正確には、同じコンテキストで、インライン化によりコンテキストに複数のメソッドが含まれる可能性があるため)、次のthrowように高速でシンプルです。goto(もちろん、JITコンパイル後)です。

ただし、catchブロックがスタックのどこかより深い場合、JVMはスタックフレームを巻き戻す必要があり、これには大幅に時間がかかる可能性があります。synchronized巻き戻しは、削除されたスタックフレームが所有するモニターの解放を意味するため、関与するブロックまたはメソッドがある場合、さらに時間がかかります。


上記のステートメントは適切なベンチマークで確認できましたが、幸いにも、HotSpotのパフォーマンスエンジニアAlexey Shipilev:The Exceptional Performance of Lil 'Exceptionの投稿ですべての側面がすでに完全にカバーされているため、これを行う必要はありません。


8
記事で説明し、ここで触れたように、例外は、例外のスロー/キャッチのコストは呼び出しの深さに大きく依存するということです。ここでのポイントは、「例外は高価です」という文は実際には正しくないということです。より正しいステートメントは、例外は「高くつく」可能性があるということです。正直なところ、(記事のように)「本当に例外的なケース」にのみ例外を使用することは、あまりにも強烈な言葉遣いだと思います。これらは通常のリターンフロー以外のほとんどすべてに最適であり、実際のアプリケーションでこのように使用することによるパフォーマンスへの影響を検出することは困難です。
JimmyJames

14
例外のオーバーヘッドを定量化することは価値があります。このかなり網羅的な記事で報告された最悪のケース(実際に照会されるスタックトレースで1000スタックフレームの深さの動的例外をスローしてキャッチする)でも、80マイクロ秒かかります。システムが毎秒数千の例外を処理する必要がある場合、これは重要ですが、それ以外の場合は心配する必要はありません。そして、それは最悪のケースです。スタックトレースが少し正気である場合、またはスタックトレースをクエリしない場合、1秒あたり100万近くの例外を処理できます。
メリトン

13
例外が「高価」であることを読むと、多くの人が「何に比べて高価」であるかを尋ねるのを止めることはなく、「プログラムの高価な部分」であると仮定します。
メリトン

2
ここで言及されていない部分が1つあります。最適化が適用されないようにするための潜在的なコストです。極端な例は、JVMが「混乱する」スタックトレースを回避するためにインライン化しないことですが、例外の存在または不在によってC ++での最適化が行われたり中断されたりする(マイクロ)ベンチマークを見たことがあります。
Matthieu M.

3
@MatthieuM。例外とtry / catchブロックは、JVMのインライン化を妨げません。コンパイルされたメソッドの場合、実際のスタックトレースは、メタデータとして保存された仮想スタックフレームテーブルから再構築されます。try / catchと互換性のないJIT最適化を思い出せません。try / catch構造自体はメソッドコードに何も追加せず、コードとは別に例外テーブルとしてのみ存在します。
アパンギン

72

ほとんどのThrowableコンストラクタの最初の操作は、スタックトレースを入力することです。を入力することです。

ただし、スタックトレースを無効にするフラグを持つ保護されたコンストラクタがあります。このコンストラクタは拡張時にアクセスできますExceptionも。カスタムの例外タイプを作成すると、スタックトレースの作成を回避して、情報を少なくしてパフォーマンスを向上させることができます。

通常の方法で任意のタイプの単一の例外を作成する場合、スタックトレースを埋めるオーバーヘッドなしに何度でも再スローできます。ただし、そのスタックトレースは、特定のインスタンスでスローされた場所ではなく、構築された場所を反映します。

Javaの現在のバージョンでは、スタックトレースの作成を最適化する試みがいくつか行われています。ネイティブコードが呼び出されてスタックトレースに入力します。これにより、トレースが軽量のネイティブ構造で記録されます。対応するJava StackTraceElementオブジェクトがこのレコードから遅延作成されるのはgetStackTrace()printStackTrace()トレースを必要とする、または他のメソッドが呼び出されます。

スタックトレースの生成を排除する場合、他の主なコストは、スローとキャッチの間でスタックを巻き戻すことです。例外がキャッチされる前に遭遇する介在フレームが少ないほど、これは速くなります。

例外が本当に例外的なケースでのみスローされるようにプログラムを設計し、このような最適化を正当化するのは困難です。


3
コンストラクタへのリンク:docs.oracle.com/javase/8/docs/api/java/lang/...

25

Exceptionsについては、こちらの記事をご覧ください。

http://shipilev.net/blog/2014/exceptional-performance/

結論は、スタックトレースの構築とスタックの巻き戻しが高価な部品であるということです。以下のコードは、1.7スタックトレースのオンとオフを切り替える機能を利用しています。次に、これを使用して、さまざまなシナリオのコストを確認できます

以下は、オブジェクト作成のみのタイミングです。Stringここに追加したのは、スタックが作成されていなければ、JavaExceptionオブジェクトとの作成にほとんど違いがないことですString。スタック書き込みをオンにすると、その違いは劇的です。つまり、少なくとも1桁遅くなります。

Time to create million String objects: 41.41 (ms)
Time to create million JavaException objects with    stack: 608.89 (ms)
Time to create million JavaException objects without stack: 43.50 (ms)

以下は、スローから特定の深度で100万回戻るまでにかかった時間を示しています。

|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%)|
|   16|           1428|             243| 588 (%)|
|   15|           1763|             393| 449 (%)|
|   14|           1746|             390| 448 (%)|
|   13|           1703|             384| 443 (%)|
|   12|           1697|             391| 434 (%)|
|   11|           1707|             410| 416 (%)|
|   10|           1226|             197| 622 (%)|
|    9|           1242|             206| 603 (%)|
|    8|           1251|             207| 604 (%)|
|    7|           1213|             208| 583 (%)|
|    6|           1164|             206| 565 (%)|
|    5|           1134|             205| 553 (%)|
|    4|           1106|             203| 545 (%)|
|    3|           1043|             192| 543 (%)| 

以下は、ほぼ間違いなく単純化よりも粗悪なものです...

スタック書き込みで深さ16を使用すると、オブジェクトの作成に約40%の時間がかかります。実際のスタックトレースがこの大部分を占めています。JavaExceptionオブジェクトのインスタンス化の約93%は、スタックトレースが取得されることが原因です。つまり、この場合のスタックの巻き戻しには、残りの50%の時間がかかります。

スタックトレースオブジェクトの作成をオフにすると、20%とはるかに少ない割合でオブジェクトが作成され、スタックの巻き戻しが時間の80%を占めるようになります。

どちらの場合も、スタックの巻き戻しには全体の時間の大部分がかかります。

public class JavaException extends Exception {
  JavaException(String reason, int mode) {
    super(reason, null, false, false);
  }
  JavaException(String reason) {
    super(reason);
  }

  public static void main(String[] args) {
    int iterations = 1000000;
    long create_time_with    = 0;
    long create_time_without = 0;
    long create_string = 0;
    for (int i = 0; i < iterations; i++) {
      long start = System.nanoTime();
      JavaException jex = new JavaException("testing");
      long stop  =  System.nanoTime();
      create_time_with += stop - start;

      start = System.nanoTime();
      JavaException jex2 = new JavaException("testing", 1);
      stop = System.nanoTime();
      create_time_without += stop - start;

      start = System.nanoTime();
      String str = new String("testing");
      stop = System.nanoTime();
      create_string += stop - start;

    }
    double interval_with    = ((double)create_time_with)/1000000;
    double interval_without = ((double)create_time_without)/1000000;
    double interval_string  = ((double)create_string)/1000000;

    System.out.printf("Time to create %d String objects: %.2f (ms)\n", iterations, interval_string);
    System.out.printf("Time to create %d JavaException objects with    stack: %.2f (ms)\n", iterations, interval_with);
    System.out.printf("Time to create %d JavaException objects without stack: %.2f (ms)\n", iterations, interval_without);

    JavaException jex = new JavaException("testing");
    int depth = 14;
    int i = depth;
    double[] with_stack    = new double[20];
    double[] without_stack = new double[20];

    for(; i > 0 ; --i) {
      without_stack[i] = jex.timerLoop(i, iterations, 0)/1000000;
      with_stack[i]    = jex.timerLoop(i, iterations, 1)/1000000;
    }
    i = depth;
    System.out.printf("|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%%)|\n");
    for(; i > 0 ; --i) {
      double ratio = (with_stack[i] / (double) without_stack[i]) * 100;
      System.out.printf("|%5d| %14.0f| %15.0f| %2.0f (%%)| \n", i + 2, with_stack[i] , without_stack[i], ratio);
      //System.out.printf("%d\t%.2f (ms)\n", i, ratio);
    }
  }
 private int thrower(int i, int mode) throws JavaException {
    ExArg.time_start[i] = System.nanoTime();
    if(mode == 0) { throw new JavaException("without stack", 1); }
    throw new JavaException("with stack");
  }
  private int catcher1(int i, int mode) throws JavaException{
    return this.stack_of_calls(i, mode);
  }
  private long timerLoop(int depth, int iterations, int mode) {
    for (int i = 0; i < iterations; i++) {
      try {
        this.catcher1(depth, mode);
      } catch (JavaException e) {
        ExArg.time_accum[depth] += (System.nanoTime() - ExArg.time_start[depth]);
      }
    }
    //long stop = System.nanoTime();
    return ExArg.time_accum[depth];
  }

  private int bad_method14(int i, int mode) throws JavaException  {
    if(i > 0) { this.thrower(i, mode); }
    return i;
  }
  private int bad_method13(int i, int mode) throws JavaException  {
    if(i == 13) { this.thrower(i, mode); }
    return bad_method14(i,mode);
  }
  private int bad_method12(int i, int mode) throws JavaException{
    if(i == 12) { this.thrower(i, mode); }
    return bad_method13(i,mode);
  }
  private int bad_method11(int i, int mode) throws JavaException{
    if(i == 11) { this.thrower(i, mode); }
    return bad_method12(i,mode);
  }
  private int bad_method10(int i, int mode) throws JavaException{
    if(i == 10) { this.thrower(i, mode); }
    return bad_method11(i,mode);
  }
  private int bad_method9(int i, int mode) throws JavaException{
    if(i == 9) { this.thrower(i, mode); }
    return bad_method10(i,mode);
  }
  private int bad_method8(int i, int mode) throws JavaException{
    if(i == 8) { this.thrower(i, mode); }
    return bad_method9(i,mode);
  }
  private int bad_method7(int i, int mode) throws JavaException{
    if(i == 7) { this.thrower(i, mode); }
    return bad_method8(i,mode);
  }
  private int bad_method6(int i, int mode) throws JavaException{
    if(i == 6) { this.thrower(i, mode); }
    return bad_method7(i,mode);
  }
  private int bad_method5(int i, int mode) throws JavaException{
    if(i == 5) { this.thrower(i, mode); }
    return bad_method6(i,mode);
  }
  private int bad_method4(int i, int mode) throws JavaException{
    if(i == 4) { this.thrower(i, mode); }
    return bad_method5(i,mode);
  }
  protected int bad_method3(int i, int mode) throws JavaException{
    if(i == 3) { this.thrower(i, mode); }
    return bad_method4(i,mode);
  }
  private int bad_method2(int i, int mode) throws JavaException{
    if(i == 2) { this.thrower(i, mode); }
    return bad_method3(i,mode);
  }
  private int bad_method1(int i, int mode) throws JavaException{
    if(i == 1) { this.thrower(i, mode); }
    return bad_method2(i,mode);
  }
  private int stack_of_calls(int i, int mode) throws JavaException{
    if(i == 0) { this.thrower(i, mode); }
    return bad_method1(i,mode);
  }
}

class ExArg {
  public static long[] time_start;
  public static long[] time_accum;
  static {
     time_start = new long[20];
     time_accum = new long[20];
  };
}

この例のスタックフレームは、通常見つけられるものと比べるとごくわずかです。

javapを使用してバイトコードをのぞくことができます

javap -c -v -constants JavaException.class

つまり、これは方法4の場合です...

   protected int bad_method3(int, int) throws JavaException;
flags: ACC_PROTECTED
Code:
  stack=3, locals=3, args_size=3
     0: iload_1       
     1: iconst_3      
     2: if_icmpne     12
     5: aload_0       
     6: iload_1       
     7: iload_2       
     8: invokespecial #6                  // Method thrower:(II)I
    11: pop           
    12: aload_0       
    13: iload_1       
    14: iload_2       
    15: invokespecial #17                 // Method bad_method4:(II)I
    18: ireturn       
  LineNumberTable:
    line 63: 0
    line 64: 12
  StackMapTable: number_of_entries = 1
       frame_type = 12 /* same */

Exceptions:
  throws JavaException

13

スタックトレースを使用してExceptionを作成するにはnullthrowtry-catchブロックを一緒にブロックする場合とほぼ同じ時間がかかります。ただし、スタックトレースの書き込みには、平均で5倍の時間がかかります

パフォーマンスへの影響を示すために、次のベンチマークを作成しました。-Djava.compiler=NONE実行構成にを追加して、コンパイラの最適化を無効にしました。スタックトレースの構築による影響を測定するためExceptionに、スタックフリーコンストラクターを利用するようにクラスを拡張しました。

class NoStackException extends Exception{
    public NoStackException() {
        super("",null,false,false);
    }
}

ベンチマークコードは次のとおりです。

public class ExceptionBenchmark {

    private static final int NUM_TRIES = 100000;

    public static void main(String[] args) {

        long throwCatchTime = 0, newExceptionTime = 0, newObjectTime = 0, noStackExceptionTime = 0;

        for (int i = 0; i < 30; i++) {
            throwCatchTime += throwCatchLoop();
            newExceptionTime += newExceptionLoop();
            newObjectTime += newObjectLoop();
            noStackExceptionTime += newNoStackExceptionLoop();
        }

        System.out.println("throwCatchTime = " + throwCatchTime / 30);
        System.out.println("newExceptionTime = " + newExceptionTime / 30);
        System.out.println("newStringTime = " + newObjectTime / 30);
        System.out.println("noStackExceptionTime = " + noStackExceptionTime / 30);

    }

    private static long throwCatchLoop() {
        Exception ex = new Exception(); //Instantiated here
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            try {
                throw ex; //repeatedly thrown
            } catch (Exception e) {

                // do nothing
            }
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newExceptionLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Exception e = new Exception();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newObjectLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Object o = new Object();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newNoStackExceptionLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            NoStackException e = new NoStackException();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

}

出力:

throwCatchTime = 19
newExceptionTime = 77
newObjectTime = 3
noStackExceptionTime = 15

これは、を作成することNoStackExceptionは、同じものを繰り返しスローするのとほぼ同じくらい高価であることを意味しExceptionます。またException、スタックトレースの作成と書き込みに4倍の時間がかかることも示しています。


1
開始時刻の前に1つの例外インスタンスを作成し、それを繰り返しスローしてキャッチするケースをもう1つ追加できますか?それは単に投げる+キャッチするコストを示します。
Martin Carney

@MartinCarney素晴らしい提案です!私はちょうどそれをするために私の答えを更新しました。
オースティンD

テストコードを少し調整しましたが、コンパイラーが最適化を行っているため、正確な数値を取得できません。
Martin Carney

@MartinCarneyディスカウントコンパイラの最適化の答えを更新しました
オースティンD

参考までに、Javaで正しいマイクロベンチマークを作成するにはどうすればよいですか。ヒント:これはそうではありません。
Daniel Pryden 16

4

質問のこの部分...

これを尋ねる別の方法は、例外のインスタンスを1つ作成し、それを何度もスローしてキャッチした場合、スローするたびに新しい例外を作成するよりもはるかに速いでしょうか?

例外を作成してどこかにキャッシュすることでパフォーマンスが向上するかどうかを尋ねているようです。はい、そうです。オブジェクトの作成時に書き込まれているスタックをオフにするのと同じです。

これらは私が得たタイミングです、この後に警告を読んでください...

|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%)|
|   16|            193|             251| 77 (%)| 
|   15|            390|             406| 96 (%)| 
|   14|            394|             401| 98 (%)| 
|   13|            381|             385| 99 (%)| 
|   12|            387|             370| 105 (%)| 
|   11|            368|             376| 98 (%)| 
|   10|            188|             192| 98 (%)| 
|    9|            193|             195| 99 (%)| 
|    8|            200|             188| 106 (%)| 
|    7|            187|             184| 102 (%)| 
|    6|            196|             200| 98 (%)| 
|    5|            197|             193| 102 (%)| 
|    4|            198|             190| 104 (%)| 
|    3|            193|             183| 105 (%)| 

もちろん、これに関する問題は、スタックトレースが、オブジェクトがスローされた場所ではなく、インスタンス化された場所を指していることです。


3

@AustinDの回答を出発点として、いくつかの調整を行いました。下部にあるコード。

1つの例外インスタンスが繰り返しスローされるケースを追加することに加えて、正確なパフォーマンス結果を得ることができるように、コンパイラーの最適化もオフにしました。この回答-Djava.compiler=NONEに従って、VM引数に追加しました。(Eclipseでは、実行構成→引数を編集して、このVM引数を設定します)

結果:

new Exception + throw/catch = 643.5
new Exception only          = 510.7
throw/catch only            = 115.2
new String (benchmark)      = 669.8

したがって、例外の作成には、スロー+キャッチの約5倍のコストがかかります。コンパイラがコストの多くを最適化しないと仮定します。

比較のために、最適化を無効にしない同じテスト実行を次に示します。

new Exception + throw/catch = 382.6
new Exception only          = 379.5
throw/catch only            = 0.3
new String (benchmark)      = 15.6

コード:

public class ExceptionPerformanceTest {

    private static final int NUM_TRIES = 1000000;

    public static void main(String[] args) {

        double numIterations = 10;

        long exceptionPlusCatchTime = 0, excepTime = 0, strTime = 0, throwTime = 0;

        for (int i = 0; i < numIterations; i++) {
            exceptionPlusCatchTime += exceptionPlusCatchBlock();
            excepTime += createException();
            throwTime += catchBlock();
            strTime += createString();
        }

        System.out.println("new Exception + throw/catch = " + exceptionPlusCatchTime / numIterations);
        System.out.println("new Exception only          = " + excepTime / numIterations);
        System.out.println("throw/catch only            = " + throwTime / numIterations);
        System.out.println("new String (benchmark)      = " + strTime / numIterations);

    }

    private static long exceptionPlusCatchBlock() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            try {
                throw new Exception();
            } catch (Exception e) {
                // do nothing
            }
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long createException() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Exception e = new Exception();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long createString() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Object o = new String("" + i);
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long catchBlock() {
        Exception ex = new Exception(); //Instantiated here
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            try {
                throw ex; //repeatedly thrown
            } catch (Exception e) {
                // do nothing
            }
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }
}

最適化を無効にする=素晴らしいテクニック!誰も誤解を招かないように、元の回答を編集します
オースティンD

3
純粋なインタープリターモードは実際のパフォーマンスとは関係がないため、最適化を無効にすることは、欠陥のあるベンチマークを作成することよりも優れた方法ではありません。JVMの力はJITコンパイラーですが、実際のアプリケーションの動作を反映していないものを測定する意味は何ですか?
アパンギン2016年

2
例外の作成、スロー、およびキャッチには、この「ベンチマーク」よりも多くの側面があります。この投稿を読むことを強くお勧めします。
アパンギン2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.