アノテーションと継承のコスト差

ふと、特定の処理の前後に差し込む処理を継承などを用いて実現する場合と@Beforeなどのアノテーションやリフレクションを用いて実現する際のパフォーマンス差を測定したのでメモ。たぶん、こんなベンチマークは何年も前にみんな見たことあるだろうし今さら書いても特に意味は無いのだけれど、せっかくなので。

Core2Duo メモリ3Gのマシンでそれぞれ100万回実行を10回繰り返した結果です。

No アノテーション(ms) アノテーション(キャッシュあり)(ms) 継承(ms)
1 17605 294 10
2 17312 257 9
3 17496 256 9
4 17270 255 9
5 17332 255 10
6 17372 255 8
7 17362 256 9
8 17385 257 9
9 17265 257 9
10 17215 256 10
平均 17361.4 259.8 9.2

とまぁ、継承で実装する場合を1とするとアノテーションを使って実現すると1890倍、キャッシュを用いても70倍と、かなりの差がある。
ただし、一回当たりの実行時間はアノテーションを用いても 0.01736 ms。キャッシュを用いると 0.00025 ms。

使う場所を十分検討すれば、パフォーマンスに大きな影響を与えないレベルである事も分かる。
早すぎる最適化を避け、可読性を重視するのであればアノテーションを使うほうが理にかなっているような気もします。

以下、ソースコード
何のベンチーマークかよく分からないなどのつっこみどころがあると思いますが、ひとまずこれで。

アノテーションクラス(RetentionPolicy.RUNTIMEを忘れないように)

package test;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Before {

}

ベンチマークのベースとなるクラス(コメントアウトを外すとキャッシュを使った実装になります)

package test;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public abstract class BaseClass {

//    private Map<String, List<Method>> cache = new HashMap<String, List<Method>>();

    protected void setUp() throws IllegalArgumentException,
            IllegalAccessException, InvocationTargetException {
        // if (cache.containsKey(this.getClass().getName())) {
        // List<Method> list = cache.get(this.getClass().getName());
        // for (Method method : list) {
        // method.invoke(this);
        // }
        // return;
        // }
        Method[] methods = getClass().getMethods();
        List<Method> beforeMethods = new ArrayList<Method>();
        for (Method method : methods) {
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                if (annotation.annotationType().equals(Before.class)) {
                    beforeMethods.add(method);
                    method.invoke(this);
                }
            }
        }
//        cache.put(this.getClass().getName(), beforeMethods);
    }

    public void execute() throws IllegalArgumentException,
            IllegalAccessException, InvocationTargetException {
        for (int x = 0; x < 10; x++) {
            long start = System.currentTimeMillis();
            int loopCount = 1000000;
            for (int i = 0; i < loopCount; i++) {
                setUp();
            }
            System.out.println(System.currentTimeMillis() - start);
            exec();

            start = System.currentTimeMillis();
            for (int i = 0; i < loopCount; i++) {
                tearDown();
            }
            System.out.println(System.currentTimeMillis() - start);
        }
    }

    public abstract void exec();

    public abstract void tearDown();
}

ベンチマークするクラス

package test;

import java.lang.reflect.InvocationTargetException;

public class TestClass extends BaseClass {

    public int i = 0;

    @Override
    public void exec() {
    }

    @Before
    public void beforeMethod() {
        i++;
    }

    public static void main(String[] args) throws IllegalArgumentException,
            IllegalAccessException, InvocationTargetException {
        new TestClass().execute();
    }

    @Override
    public void tearDown() {
        i++;
    }
}