アノテーションによるアサーション(その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日追記:

まぁ、裏がとれたからよしとしよう。