質問:Javaでの例外処理は実際には遅いですか?
Googleの多くの結果と同様に、従来の知恵は、Javaの通常のプログラムフローには例外的なロジックを使用するべきではないと述べています。通常、2つの理由があります。
そして
この質問は#1に関するものです。
例として、このページJavaの例外処理を「非常に遅い」と説明し、そのスローさを例外メッセージ文字列の作成に関連付けます - 「この文字列は、スローされる例外オブジェクトの作成に使用されます。これは高速ではありません。」記事Javaでの効果的な例外処理「この理由は、例外処理のオブジェクト作成の側面によるもので、それによって例外のスローが本質的に遅くなる」と述べています。もう1つの理由は、スタックトレースの生成が遅くなることです。
私のテスト(32ビットLinux上でJava 1.6.0_07、Java HotSpot 10.0を使用)は、例外処理が通常のコードより遅くないことを示しています。私はいくつかのコードを実行するループ内でメソッドを実行しようとしました。メソッドの最後に、ブール値を使用して戻るまたはスロー。このように実際の処理は同じです。 JVMをウォームアップした可能性があると考えて、メソッドを異なる順序で実行し、テスト時間を平均してみました。私のすべてのテストで、スローではないにしても、スローは少なくともリターンと同じくらい速くなっていました(最大3.1%速く)。私は自分のテストが間違っていた可能性について完全にオープンにしていますが、コードサンプル、テスト比較、あるいは実際のところJavaでの例外処理を示す昨年または二年間の結果に関して何かを見たことはありません。スロー。
この道を私が導いたのは、通常の制御ロジックの一部として例外をスローした、私が使用する必要のあるAPIでした。使い方を修正したいのですが、今はできないかもしれません。その代わりに、私は彼らの前向きな考えで彼らを賛美しなければなりませんか?
紙の中ジャストインタイムコンパイルにおける効率的なJava例外処理たとえば、例外がスローされない場合でも、例外ハンドラのみが存在することで、JITコンパイラがコードを適切に最適化できなくなり、処理速度が低下するのを防ぐのに十分であると著者は示唆しています。私はまだこの理論を検証していません。
例外の実装方法によって異なります。最も簡単な方法はsetjmpとlongjmpを使うことです。つまり、CPUのすべてのレジスタがスタックに書き込まれ(すでに時間がかかります)、場合によっては他のデータを作成する必要があります。これはすべてtryステートメントで既に行われています。 throwステートメントは、スタックをアンワインドし、全てのレジスタの値(そしてVM内の可能な他の値)を復元する必要があります。したがって、tryとthrowは同じくらい遅く、それはかなり遅いのですが、例外がスローされなければ、ほとんどの場合tryブロックを終了する時間はまったくかかりません(メソッドが存在する場合はすべて自動的にクリーンアップされるため)。
Sunや他の人たちは、これはおそらく最適とは言えず、もちろんVMはどんどん速くなっていくことを認識していました。例外を実装するもう1つの方法があります。それは、それ自体を速くすることを試みます(実際には、ほとんど何も起こりません - クラスがVMによってロードされるときに必要なことはすべて行われます)。 。どのJVMがこの新しい、より良いテクニックを使っているかわかりません...
...しかし、あなたはJavaで書いているので、あとであなたのコードはある特定のシステム上の1つのJVM上でしか動かないでしょうか?他のプラットフォームや他のJVMバージョン(おそらく他のベンダのバージョン)で実行される可能性があるとしたら、誰もが高速実装を使用すると言っているのでしょうか。速いものは遅いものよりも複雑で、すべてのシステムで簡単には不可能です。あなたは携帯し続けたいですか?それから例外が速いことに頼らないでください。
tryブロックの中であなたがすることにも大きな違いがあります。 tryブロックを開いて、このtryブロック内からメソッドを呼び出さないと、tryブロックは超高速になります。これはJITが実際にスローを単純なgotoのように扱うことができるためです。例外がスローされた場合は、スタック状態を保存する必要も、スタックをアンワインドする必要もありません(キャッチハンドラにジャンプするだけでよい)。しかし、これはあなたが通常することではありません。通常、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ブロックからの減速は、バックグラウンドプロセスなどの交絡因子を排除するには小さすぎます。しかし、捕獲ブロックはすべてを殺し、それを66倍遅くしました!
私が言ったように、もしあなたがtry / catchを入れて同じメソッド(method3)の中に全部投げても結果はそれほど悪くないでしょう、しかしこれは私が頼りにしない特別なJIT最適化です。この最適化を使用しても、スローはまだかなり遅いです。だから私はあなたがここで何をしようとしているのかわかりませんが、try / catch / throwを使用するより確実にそれを行う方法があります。
nanoTime()
Java 1.5が必要で、上記のコードを書くために使ったシステムではJava 1.4しか利用できませんでした。また、実際に大きな役割を果たすことはありません。両者の唯一の違いは、一方がナノセカンド、もう一方がミリセカンドです。nanoTime
クロック操作の影響を受けません(テストコードが実行されている瞬間にユーザーまたはシステムプロセスがシステムクロックを変更しない限り、これは無関係です)。一般的にはあなたは正しい、nanoTime
もちろんより良い選択です。 - Meckitry
ブロックしますが、いいえthrow
。きみのthrow
テストは例外を投げています時間の50%それは通りますtry
。これは明らかに失敗がそうではない状況です。並外れた。これをわずか10%に削減すると、パフォーマンスへの影響が大幅に削減されます。この種のテストの問題点は、例外をまったく使わないようにすることです。例外の使用は、例外的なケース処理のために、テストが示すものよりもはるかに優れたパフォーマンスを発揮します。 - Natereturn
。それは体の真ん中のどこかに、おそらく手術の真ん中(それは今のところ50%しか完了していない)の方法さえも残します。catch
ブロックは上方向に20スタックフレームであるかもしれません(try
block、method1を呼び出し、method2を呼び出し、method2を呼び出し、method2を呼び出し、method20では、操作の途中で例外がスローされます。スタックは20フレーム上方に巻き戻し、未完成の操作はすべて元に戻し(操作を半分にしてはいけません)、CPUレジスタはクリーンな状態にする必要があります。これはすべて時間がかかります。 - 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はmethod3と同じですが、new Integer(1)
するよりもthrow new Exception()
。
method 5はmethod 3と似ていますが、new Exception()
投げずに。
method6はmethod3と似ていますが、新しい例外を作成するのではなく、事前に作成された例外(インスタンス変数)をスローする点が異なります。
Javaでは、例外をスローするための費用の大部分は、スタックトレースの収集に費やされる時間です。これは、例外オブジェクトが作成されるときに発生します。例外をスローするための実際のコストは、大きいものの、例外を作成するためのコストよりもかなり少なくなります。
AlekseyShipilëvがやりました非常に徹底的な分析彼はさまざまな条件の組み合わせの下でJava例外をベンチマークしています。
彼はまた、それらをさまざまなレベルのエラー頻度でエラーコードをチェックするパフォーマンスと比較します。
結論(彼の投稿から逐語的に引用)は以下のとおりです。
本当に例外的な例外は美しく実行されます。設計どおりにそれらを使用し、通常のコードによって処理される圧倒的に多数の例外的でないケースの中で本当に例外的なケースのみを伝達する場合、例外を使用することはパフォーマンス上の利点です。
例外のパフォーマンスコストには、主に2つの要素があります。スタックトレース構築Exceptionがインスタンス化されているときスタック巻き戻し例外の発生中
スタックトレースの建設費はスタックの深さに比例します例外のインスタンス化の瞬間に。このスローメソッドが呼び出されるスタックの深さを誰が知っているのでしょうか。たとえあなたがスタックトレース生成をオフにしたり例外をキャッシュしたとしても、あなたはパフォーマンスコストのこの部分を取り除くことができるだけです。
スタックの巻き戻しコストは、コンパイルされたコード内で例外ハンドラを近づけることがどれほど幸運であるかによって異なります。深い例外ハンドラの検索を避けるために慎重にコードを構築することは、おそらく私たちが幸運になるのを助けます。
両方の影響を排除した場合、例外のパフォーマンスコストはローカルブランチのコストとなります。たとえどんなにきれいに聞こえても、それはあなたが通常の制御フローとして例外を使うべきであるという意味ではありません。あなたはコンパイラを最適化するのはやめられない!例外の頻度が高い、本当に例外的な場合にのみそれらを使用してください。償却する実際の例外を発生させる可能性のある不運なコスト
楽観的な経験則は、10 ^ -4例外の頻度は十分に優れています。もちろん、それは例外自体の重い重み、例外ハンドラでとられる正確なアクションなどに依存します。
結局のところ、例外がスローされない場合はコストがかからないため、例外条件が十分にまれな場合は、例外処理を使用するよりも高速に処理されます。if
毎回。全文は読む価値があります。
私の答えは、残念ながら、ここに投稿するには長すぎます。それではここで要約してあなたに紹介しましょう。http://www.fuwjax.com/how-slow-are-java-exceptions/粗末な詳細については。
ここでの本当の問題は、「失敗したことがないコード」と比較して「失敗が例外として報告されるのはどれくらい遅いのか」ということではありません。受け入れられた応答はあなたが信じるかもしれないように。そうではなく、「失敗は他の方法で報告された失敗と比べてどれくらい遅いのか」と質問する必要があります。一般に、失敗を報告する他の2つの方法は、センチネル値または結果ラッパーのどちらかです。
Sentinel値は、成功した場合に1つのクラスを返し、失敗した場合に別のクラスを返すという試みです。あなたはほとんど例外を投げるのではなく例外を返すことと考えることができます。これには、成功オブジェクトを持つ共有親クラスが必要です。それから、 "instanceof"チェックを実行して、成功または失敗の情報を取得するために2つのキャストを行います。
型安全性の危険性があるため、Sentinelの値は例外よりも速いのですが、約2倍に過ぎません。今、それは多くのように思えるかもしれませんが、その2倍は実装の違いのコストをカバーするだけです。実際には、このページの他の場所にあるサンプルコードのように、失敗する可能性のあるメソッドは、いくつかの算術演算子よりもはるかに興味深いため、この係数ははるかに低くなります。
結果一方、ラッパーは型安全を犠牲にしません。成功と失敗の情報を単一のクラスにまとめます。そのため、 "instanceof"の代わりに、 "isSuccess()"と、成功オブジェクトと失敗オブジェクトの両方のゲッターを提供します。ただし、結果オブジェクトは約2倍です。もっとゆっくり例外を使用するよりも。毎回新しいラッパーオブジェクトを作成するほうが、例外をスローするよりもはるかにコストがかかることがわかります。
それに加えて、例外は、メソッドが失敗する可能性があることを示す方法として提供されている言語です。どのメソッドが常に(ほとんど)動作することが期待され、どのメソッドが失敗を報告することが期待されるかをAPIだけで判断する方法は他にありません。
例外はセンチネルよりも安全で、結果オブジェクトよりも速く、そしてどちらよりも驚くほどではありません。私は/ 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() );
}
Core i7、8GB RAM上のJava 1.6.0_45での出力:
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);
}
}
public class NoStackTraceThrowable extends Throwable { public NoStackTraceThrowable() { super("my special throwable", null, false, false); } }
- Greg
これらのトピックが関連しているかどうかわからないが、私はかつて現在のスレッドのスタックトレースに頼る1つのトリックを実装したいと思いました。インスタンス化されたクラスの内部でインスタンス化を引き起こしたメソッドの名前を発見したいと思いました。私は完全にそれをあきらめた)。だから私はその呼び出しを発見したThread.currentThread().getStackTrace()
です極めて遅い(ネイティブのためdumpThreads
内部的に使用するメソッド)
だからJavaThrowable
対応して、ネイティブメソッドがありますfillInStackTrace
。キラーだと思うcatch
前述のブロックがどういうわけかこのメソッドの実行をトリガーします。
しかし別の話をしましょう…
Scalaでは、いくつかの機能的機能がJVMを使ってコンパイルされています。ControlThrowable
拡張する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がこれを行う理由は、(まだ)JVMで利用できない他の言語の機能をエミュレートするためです。制御フローに例外を使用する習慣を身に付けるべきではありません。特にパフォーマンス上の理由ではありません。あなた自身が#2で言及したように、あなたはこのようにあなたのコードの中の重大なバグを隠す危険があります、そしてそれは新しいプログラマーのために保守することがより難しいでしょう。
Javaでのマイクロベンチマークは、特にJIT領域に入ったときには、驚くほどうまくいきません(私が言われました)ので、実際には例外を使用することが「戻る」よりも速いのではないかと思います。たとえば、テストで2〜5スタックフレームのどこかにあると思いますか。今度は、コードがJBossによってデプロイされたJSFコンポーネントによって呼び出されることを想像してください。今、あなたは数ページの長さのスタックトレースを持っているかもしれません。
テストコードを投稿できますか。
私はJVM 1.5でいくつかの性能テストをしました、そして、例外を使うことは少なくとも2倍遅くなりました。平均して:例外を除いて3倍以上(3倍)の非常に小さい方法での実行時間。例外をキャッチしなければならなかったごくわずかなループでは、セルフタイムが2倍に増えました。
実稼動コードと同様にマイクロベンチマークでも同じような数字を見ました。
例外は確実にではない頻繁に呼ばれることすべてに使用されます。 1秒間に何千もの例外をスローすると、巨大なボトルネックが発生します。
たとえば、 "Integer.ParseInt(...)"を使用して非常に大きなテキストファイル内のすべての不適切な値を見つける - これは非常に悪い考えです。 (私はこの実用的な方法を見ました殺します製品コードでのパフォーマンス)
例外を使用してユーザーGUIフォームに不正な値を報告します。おそらくパフォーマンスの観点からはそれほど悪くありません。
その良い設計方法であろうとなかろうと、私はルールをたどります:エラーが正常/予想されるものであれば、それから戻り値を使います。異常な場合は、例外を使用してください。例:ユーザー入力を読むと、悪い値が普通です - エラーコードを使用してください。内部のユーティリティ関数に値を渡して、悪い値はコードを呼び出すことによってフィルタリングされるべきです - 例外を使用してください。
しばらく前に、2つのアプローチを使用して文字列を整数に変換する相対的なパフォーマンスをテストするクラスを書きました。(1)Integer.parseInt()を呼び出して例外をキャッチする、または(2)文字列をregexと照合してparseInt()を呼び出す一致が成功した場合のみ私は可能な限り最も効率的な方法で正規表現を使用しました(つまり、ループに入る前にPatternオブジェクトとMatcherオブジェクトを作成しました)。例外からスタックトレースを印刷または保存しませんでした。
1万個の文字列のリストの場合、それらがすべて有効な数値であれば、parseInt()アプローチは正規表現アプローチの4倍の速さでした。しかし、80%の文字列しか有効でなければ、正規表現はparseInt()の2倍の速さでした。また、20%が有効であれば、例外がスローされ、80%の時間がキャッチされたので、正規表現はparseInt()の約20倍の速さでした。
正規表現によるアプローチで有効な文字列が2回処理されることを考えれば、結果には驚きました。1回は一致のため、もう1回はparseInt()のためです。しかし、それを補う以上の例外を投げてキャッチすること。このような状況は現実の世界ではそれほど頻繁には発生しませんが、発生する場合は、必ず例外キャッチ技法を使用しないでください。しかし、ユーザー入力などを検証するだけの場合は、必ずparseInt()アプローチを使用してください。
Integer.ParseInt()
私はそれを期待していますその時間の大半ユーザーの入力は正しいでしょうから、私のユースケースでは、時折例外が発生した場合はそれを回避する方法が適しています。 - markvgti
たとえ例外をスローしても遅くないとしても、それでも通常のプログラムフローのために例外をスローするのは悪い考えです。これはGOTOに似た方法で使われています...
私はそれが質問に実際に答えないと思います。例外をスローするという「従来の」知恵は、以前のバージョンのJavaにも当てはまります(&lt; 1.4)。例外を作成するには、VMがスタックトレース全体を作成する必要があります。それ以降、VMの速度を上げるために多くの変更が加えられましたが、これはおそらく改善された1つの分野です。
break
またはreturn
ではないgoto
。 - Hot Licks
HotSpotは、インライン化されている限り、システム生成の例外の例外コードを削除することができます。ただし、明示的に作成された例外とそれ以外の方法で削除されなかった例外は、スタックトレースの作成に多くの時間を費やします。オーバーライドfillInStackTrace
これがパフォーマンスに与える影響を確認する
JavaおよびC#での例外パフォーマンスには、まだ望ましいことがたくさんあります。
プログラマとしては、これは、単に実用的なパフォーマンス上の理由から、「例外はめったに発生させないようにする」という規則に従うことを強いられます。
しかし、コンピュータ科学者として、私たちはこの問題のある状態に反抗するべきです。関数をオーサリングする人は、その関数がどれほど頻繁に呼び出されるのか、あるいは成功または失敗の可能性が高いのかどうかわからないことがよくあります。発信者だけがこの情報を持っています。例外を避けようとすると、明確ではないが遅い例外バージョンしかない場合や、高速だが不格好な戻り値エラーがある場合があり、さらに他の場合はその両方が発生するという不明瞭なAPIイデオムが発生します。 。ライブラリの実装者は、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
戻り値をチェックして伝播すると、ベースラインヌル呼び出しと比較していくらかのコストが追加され、そのコストは呼び出しの深さに比例します。コールチェーンの深さが8の場合、エラー戻り値チェックバージョンは、戻り値をチェックしなかったベースラインバージョンよりも約27%遅くなりました。
これとは対照的に、例外パフォーマンスは呼び出し深度の関数ではなく、例外頻度の関数です。ただし、例外の頻度が上がるにつれて低下することは、はるかに劇的です。わずか25%のエラー頻度で、コードは24-TIMESより遅く走りました。 100%のエラー頻度では、例外バージョンはほぼ100-TIMES遅くなります。
これは、おそらく私たちの例外実装で間違ったトレードオフをしていることを私に示唆しています。コストのかかるストークウォークを回避することによって、またはそれらをコンパイラでサポートされる戻り値チェックに完全に変換することによって、例外はより速くなる可能性があります。そうするまでは、コードを高速に実行したいときには回避しています。
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"を1000回解析するなど)すぐに、パフォーマンスの違いが重要になります。
上記の@Meckiの答えを、method1がブール値と呼び出し側のメソッドのチェックを返すように変更しました。例外を何もなしに置き換えることはできないからです。 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
例外パフォーマンスに関する素晴らしい投稿は以下のとおりです。
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
そして戻ってboolean
Cのようなメソッド呼び出しからのフラグ)あなたがそれらを投げる頻度に応じて、あなたのアプリケーションのパフォーマンスを10倍-100倍向上させます。
プログラムでデータをチェックすることに対する例外のスピードについての私の意見
多くのクラスは、文字列から値への変換プログラム(スキャナ/パーサ)、有名なライブラリ、そして有名なライブラリも持っていました。
通常形をしている
class Example {
public static Example Parse(String input) throws AnyRuntimeParsigException
...
}
例外名はほんの一例であり、通常はチェックされていない(ランタイム)ので、宣言は私の絵にすぎません
時々二番目の形が存在する:
public static Example Parse(String input, Example defaultValue)
投げない
2番目のものが利用できない場合(またはプログラマがあまりにも少ない数の文書を読んで最初のものだけを使用する場合)は、そのようなコードを正規表現で書いてください。正規表現はかっこいい、政治的に正しい、など。
Xxxxx.regex(".....pattern", src);
if(ImTotallySure)
{
Example v = Example.Parse(src);
}
このコードを使うことで、プログラマは例外のコストをかけません。しかし、正規表現の非常に高いコストと例外の小さなコストが対比される場合もあります。
私はほとんどいつもそのような文脈で使います
try { parse } catch(ParsingException ) // concrete exception from javadoc
{
}
stacktraceなどを分析せずに、私はあなたの講義の後にかなりの速さを信じます。
例外を恐れてはいけません
例外が通常のリターンよりも遅いのはなぜですか。
スタックトレースを端末に出力したり、ファイルなどに保存したりしない限り、catchブロックは他のコードブロックよりも多くの作業を行いません。それで、「throw new my_cool_error()」がなぜそんなに遅いのか想像できません。
良い質問です。このトピックに関するさらなる情報を楽しみにしています!
exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow
- その理由について、完全かつ詳細な説明をする。そして彼は男だった書きましたJavaライブラリしたがって、クラスを定義するのは彼です。 API契約/ビルKにこれに同意してください。 - vaxquis