アノテーションによるアサーション(その9 インラインか、メソッド呼び出しか、それが問題だ編)
2009.05.02 現在のバージョン(ver0.0.8) までは、アサーション用のバイトコードをインライン展開する形でバイトコードをウィービングしていました。理由は、そのほうが高速に動作すると考えたからです。しかし、アサーション用のコード全体が毎回ウィービングされるのも PermGen の無駄遣いな気がします。アサーションを行う処理をメソッドとして用意してそれを呼び出す方式にしてしまえば PermGen は節約できそうです。しかし、メソッド呼び出しのオーバーヘッドが気になります。どうしたものか。
ということで、ver0.0.8にて、ウィービングしない版のバイトコードとウィービングした版のバイトコードを見比べてみました。
テスト用ソース
import jp.objectfanatics.assertion.constraints.NotNull; public class Sample { public void paramTest(@NotNull String a) { } @NotNull public String returnValueTest() { return ""; } }
バイトコード
ウィービングなしのバイトコード(619 bytes)
// Compiled from Sample.java (version 1.5 : 49.0, super bit) public class Sample { // Method descriptor #6 ()V // Stack: 1, Locals: 1 public Sample(); 0 aload_0 [this] 1 invokespecial java.lang.Object() [1] 4 return Line numbers: [pc: 0, line: 3] Local variable table: [pc: 0, pc: 5] local: this index: 0 type: Sample // Method descriptor #13 (Ljava/lang/String;)V // Stack: 0, Locals: 2 public void paramTest(@jp.objectfanatics.assertion.constraints.NotNull java.lang.String a); 0 return Line numbers: [pc: 0, line: 5] Local variable table: [pc: 0, pc: 1] local: this index: 0 type: Sample [pc: 0, pc: 1] local: a index: 1 type: java.lang.String // Method descriptor #19 ()Ljava/lang/String; // Stack: 1, Locals: 1 @jp.objectfanatics.assertion.constraints.NotNull public java.lang.String returnValueTest(); 0 ldc <String ""> [2] 2 areturn Line numbers: [pc: 0, line: 9] Local variable table: [pc: 0, pc: 3] local: this index: 0 type: Sample }
アサーションコードがインラインで記述されたされた場合のバイトコード(948 bytes)
// Compiled from Sample.java (version 1.5 : 49.0, super bit) @jp.objectfanatics.assertion.weaver.Woven public class Sample { // Method descriptor #6 ()V // Stack: 1, Locals: 1 public Sample(); 0 aload_0 [this] 1 invokespecial java.lang.Object() [1] 4 return Line numbers: [pc: 0, line: 3] Local variable table: [pc: 0, pc: 5] local: this index: 0 type: Sample // Method descriptor #13 (Ljava/lang/String;)V // Stack: 4, Locals: 2 public void paramTest(@jp.objectfanatics.assertion.constraints.NotNull java.lang.String a); 0 aload_1 [a] 1 aconst_null 2 if_acmpne 20 5 new java.lang.AssertionError [28] 8 dup 9 ldc <Class java.lang.Object> [4] 11 ldc <String "parameter 0(zero base) is null."> [30] 13 invokevirtual java.lang.Class.cast(java.lang.Object) : java.lang.Object [36] 16 invokespecial java.lang.AssertionError(java.lang.Object) [39] 19 athrow 20 return Line numbers: [pc: 20, line: 5] Local variable table: [pc: 0, pc: 21] local: this index: 0 type: Sample [pc: 0, pc: 21] local: a index: 1 type: java.lang.String // Method descriptor #19 ()Ljava/lang/String; // Stack: 5, Locals: 3 @jp.objectfanatics.assertion.constraints.NotNull public java.lang.String returnValueTest(); 0 ldc <String ""> [2] 2 goto 5 5 astore_2 6 aload_2 7 aconst_null 8 if_acmpne 26 11 new java.lang.AssertionError [28] 14 dup 15 ldc <Class java.lang.Object> [4] 17 ldc <String "return value is null."> [41] 19 invokevirtual java.lang.Class.cast(java.lang.Object) : java.lang.Object [43] 22 invokespecial java.lang.AssertionError(java.lang.Object) [45] 25 athrow 26 aload_2 27 areturn Line numbers: [pc: 0, line: 9] Local variable table: [pc: 0, pc: 5] local: this index: 0 type: Sample }
ウィービングなしのバイトコードが 619 バイト、アサーションコードがインラインで記述されたされた場合のバイトコードが 948 バイトということで、アサーション用のコードによってバイトコードが 329 バイト増加したことが確認できます。次に、メソッド呼び出し形式を試してみます。
アサーションコードがアサーション用スタティックメソッド呼び出しとして記述されたされた場合のバイトコード(882bytes)
// Compiled from Sample.java (version 1.5 : 49.0, super bit) @jp.objectfanatics.assertion.weaver.Woven public class Sample { // Method descriptor #6 ()V // Stack: 1, Locals: 1 public Sample(); 0 aload_0 [this] 1 invokespecial java.lang.Object() [1] 4 return Line numbers: [pc: 0, line: 3] Local variable table: [pc: 0, pc: 5] local: this index: 0 type: Sample // Method descriptor #13 (Ljava/lang/String;)V // Stack: 2, Locals: 2 public void paramTest(@jp.objectfanatics.assertion.constraints.NotNull java.lang.String a); 0 aload_1 [a] 1 iconst_0 2 invokestatic jp.objectfanatics.assertion.constraints.RuntimeConstraintAssertor.assertParameterNotNull(java.lang.Object, int) : void [32] 5 return Line numbers: [pc: 5, line: 5] Local variable table: [pc: 0, pc: 6] local: this index: 0 type: Sample [pc: 0, pc: 6] local: a index: 1 type: java.lang.String // Method descriptor #19 ()Ljava/lang/String; // Stack: 2, Locals: 3 @jp.objectfanatics.assertion.constraints.NotNull public java.lang.String returnValueTest(); 0 ldc <String ""> [2] 2 goto 5 5 astore_2 6 aload_2 7 invokestatic jp.objectfanatics.assertion.constraints.RuntimeConstraintAssertor.assertReturnValueNotNull(java.lang.Object) : void [36] 10 aload_2 11 areturn Line numbers: [pc: 0, line: 9] Local variable table: [pc: 0, pc: 5] local: this index: 0 type: Sample }
インライン展開方式の 948 バイトに対して、メソッド呼び出し方式は 882 バイト。その差は 66 バイト。誤差の範囲と言えるような言えないような、微妙な数字です、、、。エラーメッセージ用の文字列が長くなったとしても、コンスタントプール側のポインタを参照するだけなのでメモリを喰うわけじゃないし。
よく考えてみれば、そもそもおいらはこのライブラリを作成する前は、null チェック用コードを直に書いました。なので、インライン展開版でもメモリフットプリント的には問題ないんですよね、、、。
それよりも、リファクタリング好きのおいらは繰り返し系のコードはほとんどすべて extract method してしまうので、メソッド呼び出し時のオーバーヘッドの方が問題かもしれない。しかし、それでも誤差の範囲かな。
また、メソッド呼び出し方式の場合は、AssertionError が出た場合の printStackTrace の基点がアサーション用メソッド内部で AssertionError を呼び出している箇所になってしまいます。
以下が、メソッド呼び出し方式の場合の printStackTrace。エラーの基点が RuntimeConstraintAssertor.java:8 というライブラリ側のメソッドになっているので気持ち悪い。ユーザーは混乱してしまうかも。
下記、サンプルを用いての例
※注:下記の Sample クラスは、冒頭の Sample クラスとは違うテキトーに作ったクラスです。m(_ _)m
Exception in thread "main" java.lang.AssertionError: parameter 0(zero base) is null. at jp.objectfanatics.assertion.constraints.RuntimeConstraintAssertor.assertParameterNotNull(RuntimeConstraintAssertor.java:8) at jp.objectfanatics.assertion.weaver.Sample.set(Sample.java) at jp.objectfanatics.assertion.weaver.Sample.main(Sample.java:11)
以下は、インライン方式の場合の printStackTrace。
Exception in thread "main" java.lang.AssertionError: parameter 0(zero base) is null. at jp.objectfanatics.assertion.weaver.Sample.set(Sample.java) at jp.objectfanatics.assertion.weaver.Sample.main(Sample.java:11)
Sample.java:11 のコードは以下の set(null) の箇所で、まさにバグが埋め込まれている箇所。このほうがわかりやすい。
10: public static void main(String[] args) { 11: set(null); // <- ここで null チェックのアサーションエラーが発生する。 12: }
結論。元のままでOK
(´・ω・`)ショボーン
-
- -
2011年4月12日追記:
- メソッドを用意する方法だと実行時にライブラリのリンクが必要になるのでだめですね。
- アノテーション自体も現在はRetentionがRuntimeなので、リフレクションとかで呼ばれると実行環境で落ちます。それが問題になる場合は、実行環境にjarを追加して対応する必要があります。現在開発中のバージョンではその問題が改善される予定です。
- -
- アノテーションによるアサーション(その1)
- アノテーションによるアサーション(その2 mavenプラグイン編)
- アノテーションによるアサーション(その3 java agent 編)
- アノテーションによるアサーション(その4 eclpse 編)
- アノテーションによるアサーション(その5 訂正編)
- アノテーションによるアサーション(その6 @Max, @Min 編)
- アノテーションによるアサーション(その7 @NotEmpty 編)
- アノテーションによるアサーション(その8 java5 編)
- アノテーションによるアサーション(その9 インラインか、メソッド呼び出しか、それが問題だ編)
まぁ、裏がとれたからよしとしよう。