質問:Javaの例外処理は実際に遅いですか?
Googleの多くの結果と同様に、従来の知恵は、例外的なロジックをJavaの通常のプログラムフローに使用すべきではないと述べています。通常、2つの理由があります。
そして
この質問は#1についてです。
例として、 このページ はJava例外処理を「非常に遅い」と説明し、その遅さを例外メッセージ文字列の作成に関連付けます-「この文字列は、スローされる例外オブジェクト。これは高速ではありません。」記事 Javaでの効果的な例外処理 は、「この理由は、例外処理のオブジェクト作成の側面によるものであり、それにより例外のスローが本質的に遅くなる」と述べています。もう1つの理由は、スタックトレースの生成が遅くなることです。
私のテスト(Java 1.6.0_07、Java HotSpot 10.0、32ビットLinuxを使用)は、例外処理が通常のコードより遅くないことを示しています。コードを実行するループでメソッドを実行してみました。メソッドの最後に、ブール値を使用して、returnまたはthrow。このように、実際の処理は同じです。 JVMがウォームアップしているのではないかと考えて、異なる順序でメソッドを実行し、テスト時間を平均しました。すべてのテストで、スローは少なくともリターンと同程度の速さでしたが、速くない場合は(最大3.1%高速)。私はテストが間違っている可能性を完全に受け入れていますが、Java実際に遅くする。
この道をたどるのは、通常の制御ロジックの一部として例外をスローするために使用する必要があるAPIでした。それらの使用法を修正したかったのですが、今はできないかもしれません。代わりに、彼らの前向きな考えを称賛する必要がありますか?
論文 ジャストインタイムコンパイルでの効率的なJava例外処理 で、著者は、例外がスローされなくても、例外ハンドラーの存在だけで十分であることを示唆していますJITコンパイラーがコードを適切に最適化することにより、速度が低下します。この理論はまだテストしていません。
例外の実装方法によって異なります。最も簡単な方法は、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を使用するよりも間違いなく良い方法があります。
参考までに、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では、例外をスローするための費用の大部分は、スタックトレースの収集に費やされる時間です。これは、例外オブジェクトの作成時に発生します。例外をスローする実際のコストは、大きいものの、例外を作成するコストよりもかなり低くなります。
AlekseyShipilëvは 非常に徹底的な分析 を行い、さまざまな条件の組み合わせの下でJava例外をベンチマークしました。
また、さまざまなレベルのエラー頻度でエラーコードをチェックするパフォーマンスと比較します。
結論(彼の投稿から逐語的に引用)は次のとおりです。
真に例外的な例外は美しく機能します。設計どおりにそれらを使用し、処理された圧倒的に多数の非例外的なケース間で真に例外的なケースのみを伝える場合通常のコードでは、例外を使用するとパフォーマンスが向上します。
例外のパフォーマンスコストには、2つの主要なコンポーネントがあります。スタックトレース構築例外がインスタンス化されるときとスタックのアンワインド例外のスロー中。
スタックトレースの構築コストは、例外のインスタンス化の時点でのスタックの深さに比例します。このスローメソッドが呼び出されるスタックの深さを誰が知っているのですか?スタックトレースの生成をオフにしたり、例外をキャッシュしたりしても、パフォーマンスコストのこの部分のみを取り除くことができます。
スタックの巻き戻しコストは、コンパイルされたコードで例外ハンドラを近づけることがどれだけ幸運であるかに依存します。深い例外ハンドラの参照を避けるためにコードを慎重に構造化することはおそらく幸運を得るのに役立ちます。
両方の効果を排除する必要がある場合、例外のパフォーマンスコストはローカルブランチのコストです。どんなに美しく聞こえても、使用する必要があるという意味ではありません通常の制御フローとしての例外、その場合コンパイラーの最適化に翻弄されているためです!本当に例外的な場合にのみ使用してください。例外の頻度は、実際の例外を発生させる可能性のある不幸なコストを償却します。
楽観的な経験則は、10 ^ -4例外の頻度は十分に例外的であるようです。もちろん、それは例外自体の重い重み、例外ハンドラーで実行される正確なアクションなどに依存します。
結果は、例外がスローされない場合、コストを支払わないため、例外条件が十分にまれな場合、毎回if
を使用するよりも例外処理が高速になります。完全な投稿は読む価値があります。
残念ながら、私の回答は長すぎてここに投稿できません。そこで、ここで要約し、 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つの方法は、不要なオブジェクトとネストされたループを作成することです。例外を作成するかしないかを選択できる場合は、例外を作成しないでください。例外を作成するか、常に別のオブジェクトを作成するかを選択できる場合は、例外を作成します。
@ 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);
}
}
しばらく前に、2つのアプローチを使用して文字列をintに変換する相対的なパフォーマンスをテストするクラスを作成しました。(1)Integer.parseInt()を呼び出して例外をキャッチするか、(2)マッチが成功した場合のみ。私はできる限り効率的な方法で正規表現を使用しました(つまり、ループを実行する前にPatternおよびMatcherオブジェクトを作成しました)。例外からのスタックトレースを出力または保存しませんでした。
1万個の文字列のリストについて、それらがすべて有効な数値である場合、parseInt()アプローチは正規表現アプローチの4倍の速さでした。ただし、文字列の80%のみが有効である場合、正規表現はparseInt()の2倍の速度でした。そして、20%が有効で、例外がスローされ、80%の時間をキャッチした場合、正規表現はparseInt()の約20倍の速さでした。
正規表現のアプローチが有効な文字列を2回処理することを考えると、結果に驚きました。1回は一致、もう1回はparseInt()です。しかし、それを補う以上の例外をスローしてキャッチします。このような状況は、現実の世界ではあまり頻繁に発生することはありませんが、発生する場合は、例外キャッチ手法を使用しないでください。ただし、ユーザー入力などを検証するだけの場合は、必ずparseInt()アプローチを使用してください。
これらのトピックが関連しているかどうかはわかりませんが、私はかつて現在のスレッドのスタックトレースに依存する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
ご覧のとおり、method3
とmethod4
の唯一の違いは、異なる種類の例外をスローすることです。ええ、method4
はmethod1
とmethod2
よりもまだ遅いですが、違いははるかに受け入れられます。
最初の記事ではコールスタックを走査してスタックトレースを作成する行為が高価な部分であると考え、2番目の記事ではそれを言っていないが、それがオブジェクト作成の最も高価な部分だと思います。 John Roseには 例外を高速化するためのさまざまな手法を説明した記事 があります。 (例外、スタックトレースのない例外などの事前割り当てと再利用)
それでも、これは必要な悪、最後の手段に過ぎないと考えるべきだと思います。これを行うJohnの理由は、JVMで(まだ)利用できない他の言語の機能をエミュレートするためです。制御フローに例外を使用する習慣になってはいけません。特にパフォーマンス上の理由ではありません! #2で述べたように、この方法でコードの重大なバグを隠すリスクがあり、新しいプログラマーにとっては維持が難しくなります。
Javaのマイクロベンチマークは、特にJITの領域に入ると、驚くほど正確に取得するのが難しいため、実際の生活では例外を使用するほうが「戻る」よりも速いとは本当に疑っています。たとえば、テストで2〜5個のスタックフレームがあると思われますか?ここで、JBossによってデプロイされたJSFコンポーネントによってコードが呼び出されることを想像してください。これで、数ページのスタックトレースがある場合があります。
おそらく、テストコードを投稿できますか?
JVM 1.5でパフォーマンステストをいくつか行いましたが、例外の使用は少なくとも2倍遅くなりました。平均:例外を除き、3倍(3倍)以上の些細な方法での実行時間。例外をキャッチする必要があった些細なループでは、セルフタイムが2倍に増加しました。
量産コードとマイクロベンチマークで同様の数値を見てきました。
例外は間違いなくNOT頻繁に呼び出されるものには使用する必要があります。 1秒間に数千の例外をスローすると、巨大なボトルネックが発生します。
たとえば、「Integer.ParseInt(...)」を使用して、非常に大きなテキストファイル内のすべての不適切な値を検索します。これは非常に悪い考えです。 (私はこのユーティリティメソッドを見ましたkill本番コードでのパフォーマンス)
例外を使用して、ユーザーGUIフォームで不適切な値を報告します。おそらくパフォーマンスの観点からはそれほど悪くはありません。
良いデザインのプラクティスであるかどうかに関係なく、私はルールに従います:エラーが正常/予想される場合、戻り値を使用します。異常な場合は、例外を使用してください。たとえば、ユーザー入力の読み取り、不正な値は正常です。エラーコードを使用します。内部ユーティリティ関数に値を渡すと、コードを呼び出して不正な値をフィルタリングする必要があります。例外を使用します。
例外のスローが遅くない場合でも、通常のプログラムフローに対して例外をスローすることはお勧めできません。この方法で使用すると、GOTOに類似しています...
しかし、それは本当に質問に答えていないと思います。例外をスローする「従来の」知恵は、以前のJavaバージョン(<1.4)では真実だったと思います。例外を作成するには、VMがスタックトレース全体を作成する必要があります。その後、VMで多くのことが変更され、速度が向上しました。これはおそらく改善された1つの領域です。
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倍遅くなります。
これは、おそらく例外実装で間違ったトレードオフを行っていることを示唆しています。例外は、コストのかかるストークウォークを回避するか、コンパイラがサポートする戻り値チェックに完全に変えることで、より高速になる可能性があります。コードが実行されるまで、コードを高速に実行したい場合は、それらを回避する必要があります。
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回解析するために)、パフォーマンスの違いが重要になります。
例外パフォーマンスに関する素晴らしい投稿は次のとおりです。
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、投げる頻度に応じて)
上記の@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