web-dev-qa-db-ja.com

Java例外はどのくらい遅いですか?

質問:Javaの例外処理は実際に遅いですか?

Googleの多くの結果と同様に、従来の知恵は、例外的なロジックをJavaの通常のプログラムフローに使用すべきではないと述べています。通常、2つの理由があります。

  1. それは本当に遅いです-通常のコードよりも桁違いに遅い(与えられた理由は異なります)

そして

  1. 例外的なコードではエラーのみが処理されることを期待しているため、面倒です。

この質問は#1についてです。

例として、 このページ はJava例外処理を「非常に遅い」と説明し、その遅さを例外メッセージ文字列の作成に関連付けます-「この文字列は、スローされる例外オブジェクト。これは高速ではありません。」記事 Javaでの効果的な例外処理 は、「この理由は、例外処理のオブジェクト作成の側面によるものであり、それにより例外のスローが本質的に遅くなる」と述べています。もう1つの理由は、スタックトレースの生成が遅くなることです。

私のテスト(Java 1.6.0_07、Java HotSpot 10.0、32ビットLinuxを使用)は、例外処理が通常のコードより遅くないことを示しています。コードを実行するループでメソッドを実行してみました。メソッドの最後に、ブール値を使用して、returnまたはthrow。このように、実際の処理は同じです。 JVMがウォームアップしているのではないかと考えて、異なる順序でメソッドを実行し、テスト時間を平均しました。すべてのテストで、スローは少なくともリターンと同程度の速さでしたが、速くない場合は(最大3.1%高速)。私はテストが間違っている可能性を完全に受け入れていますが、Java実際に遅くする。

この道をたどるのは、通常の制御ロジックの一部として例外をスローするために使用する必要があるAPIでした。それらの使用法を修正したかったのですが、今はできないかもしれません。代わりに、彼らの前向きな考えを称賛する必要がありますか?

論文 ジャストインタイムコンパイルでの効率的なJava例外処理 で、著者は、例外がスローされなくても、例外ハンドラーの存在だけで十分であることを示唆していますJITコンパイラーがコードを適切に最適化することにより、速度が低下します。この理論はまだテストしていません。

450
John Ellinwood

例外の実装方法によって異なります。最も簡単な方法は、setjmpとlongjmpを使用することです。つまり、CPUのすべてのレジスターがスタックに書き込まれ(既に時間がかかります)、他のデータを作成する必要がある可能性があります。これらはすべてtryステートメントで既に発生しています。 throwステートメントは、スタックをアンワインドし、すべてのレジスターの値(およびVMの他の可能な値)を復元する必要があります。したがって、tryとthrowは同様に遅く、かなり遅いですが、例外がスローされない場合、ほとんどの場合、tryブロックを終了する時間はまったくかかりません(メソッドが存在する場合、すべてが自動的にクリーンアップされるスタックに置かれるため).

Sunと他の人々は、これはおそらく最適ではないことを認識しており、もちろんVMは時間とともにますます高速になっています。例外を実装する別の方法があります。それは、それ自体を高速に試行します(実際には、一般的に試行しても何も起こりません-クラスがVMによってロードされたときに発生する必要があることはすべてすでに行われています) 。どのJVMがこの新しいより良いテクニックを使用しているかわかりません...

...しかしJavaで記述しているので、後で特定のシステムの1つのJVMでのみコードを実行できますか?他のプラットフォームまたは他のJVMバージョン(おそらく他のベンダー)で実行される可能性がある場合、誰が彼らも高速実装を使用すると言うのですか?速いものは遅いものよりも複雑であり、すべてのシステムで簡単に実現することはできません。ポータブルにしたいですか?その後、例外が高速であることに依存しないでください。

また、tryブロック内で行うことにも大きな違いがあります。 tryブロックを開いて、このtryブロック内からメソッドを呼び出さない場合、JITは実際にthrowを単純なgotoのように処理できるため、tryブロックは非常に高速になります。スタック状態を保存する必要も、例外がスローされた場合にスタックを巻き戻す必要もありません(キャッチハンドラにジャンプするだけで済みます)。ただし、これは通常行うことではありません。通常、tryブロックを開いてから、例外をスローする可能性のあるメソッドを呼び出しますよね?また、メソッド内でtryブロックを使用するだけでも、他のメソッドを呼び出さない、どのようなメソッドになりますか?数値を計算するだけですか?次に、例外が必要なのは何ですか?プログラムフローを調整するためのもっとエレガントな方法があります。単純な数学以外のほとんどすべての場合、外部メソッドを呼び出す必要がありますが、これによりローカルのtryブロックの利点がすでに失われています。

次のテストコードを参照してください。

public class Test {
    int value;


    public int getValue() {
        return value;
    }

    public void reset() {
        value = 0;
    }

    // Calculates without exception
    public void method1(int i) {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            System.out.println("You'll never see this!");
        }
    }

    // Could in theory throw one, but never will
    public void method2(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            throw new Exception();
        }
    }

    // This one will regularly throw one
    public void method3(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
        // an AND operation between two integers. The size of the number plays
        // no role. AND on 32 BIT always ANDs all 32 bits
        if ((i & 0x1) == 1) {
            throw new Exception();
        }
    }

    public static void main(String[] args) {
        int i;
        long l;
        Test t = new Test();

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            t.method1(i);
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method1 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method2(i);
            } catch (Exception e) {
                System.out.println("You'll never see this!");
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method2 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method3(i);
            } catch (Exception e) {
                // Do nothing here, as we will get here
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method3 took " + l + " ms, result was " + t.getValue()
        );
    }
}

結果:

method1 took 972 ms, result was 2
method2 took 1003 ms, result was 2
method3 took 66716 ms, result was 2

Tryブロックによるスローダウンは、バックグラウンドプロセスなどの交絡要因を除外するには小さすぎます。しかし、catchブロックはすべてを殺し、66倍遅くしました!

先ほど言ったように、try/catchを入れて同じメソッド(method3)内ですべてをスローしても、結果はそれほど悪くはありませんが、これは私が依存しない特別なJIT最適化です。そして、この最適化を使用する場合でも、スローはまだかなり遅いです。あなたがここで何をしようとしているのかわかりませんが、try/catch/throwを使用するよりも間違いなく良い方法があります。

330
Mecki

参考までに、Meckiが行った実験を拡張しました。

method1 took 1733 ms, result was 2
method2 took 1248 ms, result was 2
method3 took 83997 ms, result was 2
method4 took 1692 ms, result was 2
method5 took 60946 ms, result was 2
method6 took 25746 ms, result was 2

最初の3つはMeckiのものと同じです(私のラップトップは明らかに遅いです)。

method4は、new Integer(1)を実行するのではなく、throw new Exception()を作成することを除いて、method3と同じです。

method5はmethod3と似ていますが、new Exception()をスローせずに作成する点が異なります。

method6はmethod3と似ていますが、新しい例外を作成するのではなく、事前に作成された例外(インスタンス変数)をスローする点が異なります。

Javaでは、例外をスローするための費用の大部分は、スタックトレースの収集に費やされる時間です。これは、例外オブジェクトの作成時に発生します。例外をスローする実際のコストは、大きいものの、例外を作成するコストよりもかなり低くなります。

241
Hot Licks

AlekseyShipilëvは 非常に徹底的な分析 を行い、さまざまな条件の組み合わせの下でJava例外をベンチマークしました。

  • 新しく作成された例外と事前作成された例外
  • スタックトレースの有効化と無効化
  • 要求されたスタックトレースと要求されなかったスタックトレース
  • 最上位レベルでキャッチvsすべてのレベルで再スローvsすべてのレベルでチェーン/ラップ
  • さまざまなレベルのJava呼び出しスタックの深さ
  • インライン化最適化なしvs極端なインライン化vsデフォルト設定
  • ユーザー定義フィールドの読み取りvs読み取りなし

また、さまざまなレベルのエラー頻度でエラーコードをチェックするパフォーマンスと比較します。

結論(彼の投稿から逐語的に引用)は次のとおりです。

  1. 真に例外的な例外は美しく機能します。設計どおりにそれらを使用し、処理された圧倒的に多数の非例外的なケース間で真に例外的なケースのみを伝える場合通常のコードでは、例外を使用するとパフォーマンスが向上します。

  2. 例外のパフォーマンスコストには、2つの主要なコンポーネントがあります。スタックトレース構築例外がインスタンス化されるときとスタックのアンワインド例外のスロー中。

  3. スタックトレースの構築コストは、例外のインスタンス化の時点でのスタックの深さに比例します。このスローメソッドが呼び出されるスタックの深さを誰が知っているのですか?スタックトレースの生成をオフにしたり、例外をキャッシュしたりしても、パフォーマンスコストのこの部分のみを取り除くことができます。

  4. スタックの巻き戻しコストは、コンパイルされたコードで例外ハンドラを近づけることがどれだけ幸運であるかに依存します。深い例外ハンドラの参照を避けるためにコードを慎重に構造化することはおそらく幸運を得るのに役立ちます。

  5. 両方の効果を排除する必要がある場合、例外のパフォーマンスコストはローカルブランチのコストです。どんなに美しく聞こえても、使用する必要があるという意味ではありません通常の制御フローとしての例外、その場合コンパイラーの最適化に翻弄されているためです!本当に例外的な場合にのみ使用してください。例外の頻度は、実際の例外を発生させる可能性のある不幸なコストを償却します

  6. 楽観的な経験則は、10 ^ -4例外の頻度は十分に例外的であるようです。もちろん、それは例外自体の重い重み、例外ハンドラーで実行される正確なアクションなどに依存します。

結果は、例外がスローされない場合、コストを支払わないため、例外条件が十分にまれな場合、毎回ifを使用するよりも例外処理が高速になります。完全な投稿は読む価値があります。

56
Doval

残念ながら、私の回答は長すぎてここに投稿できません。そこで、ここで要約し、 http://www.fuwjax.com/how-slow-are-Java-exceptions/ の詳細を参照してください。

ここでの本当の質問は、「「失敗は決して失敗しないコード」と比較して「失敗は例外として報告される」ということではありません」受け入れられた応答があなたに信じさせるかもしれないので。代わりに、「他の方法で報告された障害と比較して、「障害が例外として報告される」のはどのくらい遅いか」という質問にすべきです。一般に、エラーを報告する他の2つの方法は、センチネル値または結果ラッパーを使用する方法です。

Sentinel値は、成功した場合に1つのクラスを返し、失敗した場合に別のクラスを返そうとする試みです。ほとんど例外をスローするのではなく、例外を返すと考えることができます。これには、成功オブジェクトを含む共有親クラスが必要です。その後、「instanceof」チェックを実行し、成功または失敗の情報を取得するためにいくつかのキャストを実行します。

タイプセーフティのリスクがあるため、Sentinelの値は例外よりも高速ですが、約2倍しかありません。さて、それは多くのように見えるかもしれませんが、その2倍は実装の違いのコストのみをカバーします。実際には、このページの他の場所にあるサンプルコードのように、失敗する可能性のあるメソッドはいくつかの算術演算子よりもはるかに興味深いため、係数ははるかに低くなります。

一方、結果ラッパーは、型の安全性をまったく犠牲にしません。成功と失敗の情報を単一のクラスにラップします。そのため、「instanceof」の代わりに、成功オブジェクトと失敗オブジェクトの両方に「isSuccess()」とゲッターを提供します。ただし、結果オブジェクトは、例外を使用するよりも約2倍遅いです。毎回新しいラッパーオブジェクトを作成することは、時々例外をスローするよりもはるかに高価であることがわかります。

さらに、例外とは、メソッドが失敗する可能性があることを示す方法として提供される言語です。どのメソッドが常に(ほとんど)動作すると予想され、どのメソッドが失敗を報告すると予想されるかをAPIだけから伝える他の方法はありません。

例外は、番兵よりも安全で、結果オブジェクトよりも速く、どちらよりも驚くほどではありません。 try/catchがif/elseに置き換わることを提案しているわけではありませんが、例外は、ビジネスロジックであっても失敗を報告する正しい方法です。

そうは言っても、パフォーマンスに実質的に影響を与える最も頻繁な2つの方法は、不要なオブジェクトとネストされたループを作成することです。例外を作成するかしないかを選択できる場合は、例外を作成しないでください。例外を作成するか、常に別のオブジェクトを作成するかを選択できる場合は、例外を作成します。

39
Fuwjax

@ Mecki および @ incarnate で与えられる答えを、Javaのスタックトレースを埋めることなく拡張しました。

Java 7+では、Throwable(String message, Throwable cause, boolean enableSuppression,boolean writableStackTrace)を使用できます。しかし、Java6については、 この質問に対する私の答え を参照してください

// This one will regularly throw one
public void method4(int i) throws NoStackTraceThrowable {
    value = ((value + i) / i) << 1;
    // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
    // an AND operation between two integers. The size of the number plays
    // no role. AND on 32 BIT always ANDs all 32 bits
    if ((i & 0x1) == 1) {
        throw new NoStackTraceThrowable();
    }
}

// This one will regularly throw one
public void method5(int i) throws NoStackTraceRuntimeException {
    value = ((value + i) / i) << 1;
    // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
    // an AND operation between two integers. The size of the number plays
    // no role. AND on 32 BIT always ANDs all 32 bits
    if ((i & 0x1) == 1) {
        throw new NoStackTraceRuntimeException();
    }
}

public static void main(String[] args) {
    int i;
    long l;
    Test t = new Test();

    l = System.currentTimeMillis();
    t.reset();
    for (i = 1; i < 100000000; i++) {
        try {
            t.method4(i);
        } catch (NoStackTraceThrowable e) {
            // Do nothing here, as we will get here
        }
    }
    l = System.currentTimeMillis() - l;
    System.out.println( "method4 took " + l + " ms, result was " + t.getValue() );


    l = System.currentTimeMillis();
    t.reset();
    for (i = 1; i < 100000000; i++) {
        try {
            t.method5(i);
        } catch (RuntimeException e) {
            // Do nothing here, as we will get here
        }
    }
    l = System.currentTimeMillis() - l;
    System.out.println( "method5 took " + l + " ms, result was " + t.getValue() );
}

Java 1.6.0_45、Core i7、8GB RAMでの出力:

method1 took 883 ms, result was 2
method2 took 882 ms, result was 2
method3 took 32270 ms, result was 2 // throws Exception
method4 took 8114 ms, result was 2 // throws NoStackTraceThrowable
method5 took 8086 ms, result was 2 // throws NoStackTraceRuntimeException

そのため、値を返すメソッドは、例外をスローするメソッドに比べて高速です。私見、成功とエラーの両方のフローに戻り型を使用するだけで明確なAPIを設計することはできません。スタックトレースなしで例外をスローするメソッドは、通常の例外より4〜5倍高速です。

編集:NoStackTraceThrowable.Java@ Gregに感謝

public class NoStackTraceThrowable extends Throwable { 
    public NoStackTraceThrowable() { 
        super("my special throwable", null, false, false);
    }
}
18
manikanta

しばらく前に、2つのアプローチを使用して文字列をintに変換する相対的なパフォーマンスをテストするクラスを作成しました。(1)Integer.parseInt()を呼び出して例外をキャッチするか、(2)マッチが成功した場合のみ。私はできる限り効率的な方法で正規表現を使用しました(つまり、ループを実行する前にPatternおよびMatcherオブジェクトを作成しました)。例外からのスタックトレースを出力または保存しませんでした。

1万個の文字列のリストについて、それらがすべて有効な数値である場合、parseInt()アプローチは正規表現アプローチの4倍の速さでした。ただし、文字列の80%のみが有効である場合、正規表現はparseInt()の2倍の速度でした。そして、20%が有効で、例外がスローされ、80%の時間をキャッチした場合、正規表現はparseInt()の約20倍の速さでした。

正規表現のアプローチが有効な文字列を2回処理することを考えると、結果に驚きました。1回は一致、もう1回はparseInt()です。しかし、それを補う以上の例外をスローしてキャッチします。このような状況は、現実の世界ではあまり頻繁に発生することはありませんが、発生する場合は、例外キャッチ手法を使用しないでください。ただし、ユーザー入力などを検証するだけの場合は、必ずparseInt()アプローチを使用してください。

7
Alan Moore

これらのトピックが関連しているかどうかはわかりませんが、私はかつて現在のスレッドのスタックトレースに依存する1つのトリックを実装したかったのです。インスタンス化されたクラス内でインスタンス化をトリガーするメソッドの名前を発見したかった完全にtotallyめました)。そこで、Thread.currentThread().getStackTrace()の呼び出しが極端に遅いことを発見しました(内部で使用するネイティブのdumpThreadsメソッドのため)。

したがって、Java Throwableには、対応するネイティブメソッドfillInStackTraceがあります。前に説明したkiller -catchブロックが何らかの方法でこのメソッドの実行をトリガーすると思います。

しかし、別の話をしましょう...

Scalaでは、ControlThrowableを使用してJVMでいくつかの機能機能がコンパイルされます。これは、Throwableを拡張し、次の方法でfillInStackTraceをオーバーライドします。

override def fillInStackTrace(): Throwable = this

そこで、上記のテストを適用しました(サイクル量は10減り、マシンは少し遅くなります:):

class ControlException extends ControlThrowable

class T {
  var value = 0

  def reset = {
    value = 0
  }

  def method1(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      println("You'll never see this!")
    }
  }

  def method2(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      throw new Exception()
    }
  }

  def method3(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new Exception()
    }
  }

  def method4(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new ControlException()
    }
  }
}

class Main {
  var l = System.currentTimeMillis
  val t = new T
  for (i <- 1 to 10000000)
    t.method1(i)
  l = System.currentTimeMillis - l
  println("method1 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method2(i)
  } catch {
    case _ => println("You'll never see this")
  }
  l = System.currentTimeMillis - l
  println("method2 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method4(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method4 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method3(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method3 took " + l + " ms, result was " + t.value)

}

したがって、結果は次のとおりです。

method1 took 146 ms, result was 2
method2 took 159 ms, result was 2
method4 took 1551 ms, result was 2
method3 took 42492 ms, result was 2

ご覧のとおり、method3method4の唯一の違いは、異なる種類の例外をスローすることです。ええ、method4method1method2よりもまだ遅いですが、違いははるかに受け入れられます。

7
incarnate

最初の記事ではコールスタックを走査してスタックトレースを作成する行為が高価な部分であると考え、2番目の記事ではそれを言っていないが、それがオブジェクト作成の最も高価な部分だと思います。 John Roseには 例外を高速化するためのさまざまな手法を説明した記事 があります。 (例外、スタックトレースのない例外などの事前割り当てと再利用)

それでも、これは必要な悪、最後の手段に過ぎないと考えるべきだと思います。これを行うJohnの理由は、JVMで(まだ)利用できない他の言語の機能をエミュレートするためです。制御フローに例外を使用する習慣になってはいけません。特にパフォーマンス上の理由ではありません! #2で述べたように、この方法でコードの重大なバグを隠すリスクがあり、新しいプログラマーにとっては維持が難しくなります。

Javaのマイクロベンチマークは、特にJITの領域に入ると、驚くほど正確に取得するのが難しいため、実際の生活では例外を使用するほうが「戻る」よりも速いとは本当に疑っています。たとえば、テストで2〜5個のスタックフレームがあると思われますか?ここで、JBossによってデプロイされたJSFコンポーネントによってコードが呼び出されることを想像してください。これで、数ページのスタックトレースがある場合があります。

おそらく、テストコードを投稿できますか?

7
Lars Westergren

JVM 1.5でパフォーマンステストをいくつか行いましたが、例外の使用は少なくとも2倍遅くなりました。平均:例外を除き、3倍(3倍)以上の些細な方法での実行時間。例外をキャッチする必要があった些細なループでは、セルフタイムが2倍に増加しました。

量産コードとマイクロベンチマークで同様の数値を見てきました。

例外は間違いなくNOT頻繁に呼び出されるものには使用する必要があります。 1秒間に数千の例外をスローすると、巨大なボトルネックが発生します。

たとえば、「Integer.ParseInt(...)」を使用して、非常に大きなテキストファイル内のすべての不適切な値を検索します。これは非常に悪い考えです。 (私はこのユーティリティメソッドを見ましたkill本番コードでのパフォーマンス)

例外を使用して、ユーザーGUIフォームで不適切な値を報告します。おそらくパフォーマンスの観点からはそれほど悪くはありません。

良いデザインのプラクティスであるかどうかに関係なく、私はルールに従います:エラーが正常/予想される場合、戻り値を使用します。異常な場合は、例外を使用してください。たとえば、ユーザー入力の読み取り、不正な値は正常です。エラーコードを使用します。内部ユーティリティ関数に値を渡すと、コードを呼び出して不正な値をフィルタリングする必要があります。例外を使用します。

6
James Schek

例外のスローが遅くない場合でも、通常のプログラムフローに対して例外をスローすることはお勧めできません。この方法で使用すると、GOTOに類似しています...

しかし、それは本当に質問に答えていないと思います。例外をスローする「従来の」知恵は、以前のJavaバージョン(<1.4)では真実だったと思います。例外を作成するには、VMがスタックトレース全体を作成する必要があります。その後、VMで多くのことが変更され、速度が向上しました。これはおそらく改善された1つの領域です。

3
user38051

HotSpotは、すべてインライン化されている限り、システム生成例外の例外コードを削除できます。ただし、明示的に作成された例外と削除されなかった例外は、スタックトレースの作成に多くの時間を費やします。 fillInStackTraceをオーバーライドして、これがパフォーマンスに与える影響を確認します。

JavaおよびC#での例外パフォーマンスには、多くの要望が残されています。

プログラマーとして、これは単に実用的なパフォーマンス上の理由で、「例外はまれにしか発生しない」というルールに従うことを私たちに強いています。

しかし、コンピューター科学者として、この問題のある状態に反抗する必要があります。関数を作成する人は、関数が呼び出される頻度や、成功または失敗の可能性が高いかどうかをよく知りません。発信者だけがこの情報を持っています。例外を回避しようとすると、明確ではないが遅い例外バージョンしか存在しないAPI idomが不明確になります。 。ライブラリの実装者は、2つのバージョンのAPIを作成および保守する必要があり、呼び出し元は、各状況で2つのバージョンのどちらを使用するかを決定する必要があります。

これは一種の混乱です。例外のパフォーマンスが優れていれば、これらの不格好なイディオムを避け、例外を構造化されたエラーリターン機能として使用するために使用することができます。

戻り値に近い手法を使用して実装された例外メカニズムを本当に見たいので、パフォーマンスに敏感なコードではこれに戻るため、戻り値に近いパフォーマンスを得ることができます。

例外のパフォーマンスとエラーの戻り値のパフォーマンスを比較するコードサンプルを次に示します。

パブリッククラスTestIt {

int value;


public int getValue() {
    return value;
}

public void reset() {
    value = 0;
}

public boolean baseline_null(boolean shouldfail, int recurse_depth) {
    if (recurse_depth <= 0) {
        return shouldfail;
    } else {
        return baseline_null(shouldfail,recurse_depth-1);
    }
}

public boolean retval_error(boolean shouldfail, int recurse_depth) {
    if (recurse_depth <= 0) {
        if (shouldfail) {
            return false;
        } else {
            return true;
        }
    } else {
        boolean nested_error = retval_error(shouldfail,recurse_depth-1);
        if (nested_error) {
            return true;
        } else {
            return false;
        }
    }
}

public void exception_error(boolean shouldfail, int recurse_depth) throws Exception {
    if (recurse_depth <= 0) {
        if (shouldfail) {
            throw new Exception();
        }
    } else {
        exception_error(shouldfail,recurse_depth-1);
    }

}

public static void main(String[] args) {
    int i;
    long l;
    TestIt t = new TestIt();
    int failures;

    int ITERATION_COUNT = 100000000;


    // (0) baseline null workload
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                t.baseline_null(shoulderror,recurse_depth);
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("baseline: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);
        }
    }


    // (1) retval_error
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                if (!t.retval_error(shoulderror,recurse_depth)) {
                    failures++;
                }
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("retval_error: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);
        }
    }

    // (2) exception_error
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                try {
                    t.exception_error(shoulderror,recurse_depth);
                } catch (Exception e) {
                    failures++;
                }
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("exception_error: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);              
        }
    }
}

}

結果は次のとおりです。

baseline: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 683 ms
baseline: recurse_depth 2, exception_freqeuncy 0.25 (0), time elapsed 790 ms
baseline: recurse_depth 2, exception_freqeuncy 0.5 (0), time elapsed 768 ms
baseline: recurse_depth 2, exception_freqeuncy 0.75 (0), time elapsed 749 ms
baseline: recurse_depth 2, exception_freqeuncy 1.0 (0), time elapsed 731 ms
baseline: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 923 ms
baseline: recurse_depth 5, exception_freqeuncy 0.25 (0), time elapsed 971 ms
baseline: recurse_depth 5, exception_freqeuncy 0.5 (0), time elapsed 982 ms
baseline: recurse_depth 5, exception_freqeuncy 0.75 (0), time elapsed 947 ms
baseline: recurse_depth 5, exception_freqeuncy 1.0 (0), time elapsed 937 ms
baseline: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1154 ms
baseline: recurse_depth 8, exception_freqeuncy 0.25 (0), time elapsed 1149 ms
baseline: recurse_depth 8, exception_freqeuncy 0.5 (0), time elapsed 1133 ms
baseline: recurse_depth 8, exception_freqeuncy 0.75 (0), time elapsed 1117 ms
baseline: recurse_depth 8, exception_freqeuncy 1.0 (0), time elapsed 1116 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 742 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.25 (24999999), time elapsed 743 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.5 (49999999), time elapsed 734 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.75 (99999999), time elapsed 723 ms
retval_error: recurse_depth 2, exception_freqeuncy 1.0 (99999999), time elapsed 728 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 920 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.25 (24999999), time elapsed 1121   ms
retval_error: recurse_depth 5, exception_freqeuncy 0.5 (49999999), time elapsed 1037 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.75 (99999999), time elapsed 1141   ms
retval_error: recurse_depth 5, exception_freqeuncy 1.0 (99999999), time elapsed 1130 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1218 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.25 (24999999), time elapsed 1334  ms
retval_error: recurse_depth 8, exception_freqeuncy 0.5 (49999999), time elapsed 1478 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.75 (99999999), time elapsed 1637 ms
retval_error: recurse_depth 8, exception_freqeuncy 1.0 (99999999), time elapsed 1655 ms
exception_error: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 726 ms
exception_error: recurse_depth 2, exception_freqeuncy 0.25 (24999999), time elapsed 17487   ms
exception_error: recurse_depth 2, exception_freqeuncy 0.5 (49999999), time elapsed 33763   ms
exception_error: recurse_depth 2, exception_freqeuncy 0.75 (99999999), time elapsed 67367   ms
exception_error: recurse_depth 2, exception_freqeuncy 1.0 (99999999), time elapsed 66990 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 924 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.25 (24999999), time elapsed 23775  ms
exception_error: recurse_depth 5, exception_freqeuncy 0.5 (49999999), time elapsed 46326 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.75 (99999999), time elapsed 91707 ms
exception_error: recurse_depth 5, exception_freqeuncy 1.0 (99999999), time elapsed 91580 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1144 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.25 (24999999), time elapsed 30440 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.5 (49999999), time elapsed 59116   ms
exception_error: recurse_depth 8, exception_freqeuncy 0.75 (99999999), time elapsed 116678 ms
exception_error: recurse_depth 8, exception_freqeuncy 1.0 (99999999), time elapsed 116477 ms

戻り値を確認して伝播すると、ベースラインnullコールに対してコストが追加され、そのコストはコールの深さに比例します。コールチェーンの深さが8の場合、エラー戻り値のチェックバージョンは、戻り値をチェックしなかったバスラインバージョンよりも約27%遅くなりました。

それに対して、例外のパフォーマンスは、呼び出しの深さの関数ではなく、例外の頻度の関数です。ただし、例外頻度の増加に伴う劣化はさらに劇的です。わずか25%のエラー頻度で、コードは24倍遅く実行されました。エラー頻度が100%の場合、例外バージョンはほぼ100倍遅くなります。

これは、おそらく例外実装で間違ったトレードオフを行っていることを示唆しています。例外は、コストのかかるストークウォークを回避するか、コンパイラがサポートする戻り値チェックに完全に変えることで、より高速になる可能性があります。コードが実行されるまで、コードを高速に実行したい場合は、それらを回避する必要があります。

3
David Jeske

Integer.parseIntを次のメソッドと比較してみましょう。このメソッドは、例外をスローする代わりに、解析できないデータの場合にデフォルト値を返すだけです。

  public static int parseUnsignedInt(String s, int defaultValue) {
    final int strLength = s.length();
    if (strLength == 0)
      return defaultValue;
    int value = 0;
    for (int i=strLength-1; i>=0; i--) {
      int c = s.charAt(i);
      if (c > 47 && c < 58) {
        c -= 48;
        for (int j=strLength-i; j!=1; j--)
          c *= 10;
        value += c;
      } else {
        return defaultValue;
      }
    }
    return value < 0 ? /* übergebener wert > Integer.MAX_VALUE? */ defaultValue : value;
  }

両方のメソッドを「有効な」データに適用する限り、どちらもほぼ同じ速度で動作します(Integer.parseIntはより複雑なデータを処理できますが)。ただし、無効なデータを解析しようとするとすぐに(たとえば、「abc」を1.000.000回解析するために)、パフォーマンスの違いが重要になります。

2
inflamer

例外パフォーマンスに関する素晴らしい投稿は次のとおりです。

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

インスタンス化と既存の再利用、スタックトレースありとなしなど:

Benchmark                            Mode   Samples         Mean   Mean error  Units

dynamicException                     avgt        25     1901.196       14.572  ns/op
dynamicException_NoStack             avgt        25       67.029        0.212  ns/op
dynamicException_NoStack_UsedData    avgt        25       68.952        0.441  ns/op
dynamicException_NoStack_UsedStack   avgt        25      137.329        1.039  ns/op
dynamicException_UsedData            avgt        25     1900.770        9.359  ns/op
dynamicException_UsedStack           avgt        25    20033.658      118.600  ns/op

plain                                avgt        25        1.259        0.002  ns/op
staticException                      avgt        25        1.510        0.001  ns/op
staticException_NoStack              avgt        25        1.514        0.003  ns/op
staticException_NoStack_UsedData     avgt        25        4.185        0.015  ns/op
staticException_NoStack_UsedStack    avgt        25       19.110        0.051  ns/op
staticException_UsedData             avgt        25        4.159        0.007  ns/op
staticException_UsedStack            avgt        25       25.144        0.186  ns/op

スタックトレースの深さに応じて:

Benchmark        Mode   Samples         Mean   Mean error  Units

exception_0000   avgt        25     1959.068       30.783  ns/op
exception_0001   avgt        25     1945.958       12.104  ns/op
exception_0002   avgt        25     2063.575       47.708  ns/op
exception_0004   avgt        25     2211.882       29.417  ns/op
exception_0008   avgt        25     2472.729       57.336  ns/op
exception_0016   avgt        25     2950.847       29.863  ns/op
exception_0032   avgt        25     4416.548       50.340  ns/op
exception_0064   avgt        25     6845.140       40.114  ns/op
exception_0128   avgt        25    11774.758       54.299  ns/op
exception_0256   avgt        25    21617.526      101.379  ns/op
exception_0512   avgt        25    42780.434      144.594  ns/op
exception_1024   avgt        25    82839.358      291.434  ns/op

その他の詳細(JITのx64アセンブラーを含む)については、元のブログ投稿を参照してください。

つまり、Hibernate/Spring/etc-EE-shitは例外(xD)と例外からのアプリ制御フローの書き換え(continure/breakに置き換えて、メソッド呼び出しからCのようにbooleanフラグを返す)により、アプリケーションのパフォーマンスが向上することを意味します10x-100x、投げる頻度に応じて)

1
gavenkoa

上記の@Meckiの答えを変更して、method1がブール値と呼び出しメソッドのチェックを返すようにしました。これは、Exceptionを何も置き換えることができないためです。 2回実行した後、method1は依然として最速であるか、method2と同じ速さでした。

コードのスナップショットは次のとおりです。

// Calculates without exception
public boolean method1(int i) {
    value = ((value + i) / i) << 1;
    // Will never be true
    return ((i & 0xFFFFFFF) == 1000000000);

}
....
   for (i = 1; i < 100000000; i++) {
            if (t.method1(i)) {
                System.out.println("Will never be true!");
            }
    }

および結果:

実行1

method1 took 841 ms, result was 2
method2 took 841 ms, result was 2
method3 took 85058 ms, result was 2

実行2

method1 took 821 ms, result was 2
method2 took 838 ms, result was 2
method3 took 85929 ms, result was 2
0
inder