実装パターンに見るMethodTimer


ケント・ベックの実装パターンの付録にある、パフォーマンス測定フレームワークのMethodTimerを写経した。

MethodTimerは以下のように使うと、

    MethodsTimer tester = new MethodsTimer(SetVsArrayList.class.getDeclaredMethods());
    tester.report();

コレクションクラスなどの要素数の増加に伴うパフォーマンス劣化状況をレポートする。以下のような出力が得られ、setMembershipメソッドに対して、要素数が1,10,100・・と増加していった場合の所要時間が分かる。

setMembership	50.12	75.12	50.42	64.30	70.05	79.49


MethodTimerは以下

package etc9;

import java.lang.reflect.Method;

public class MethodsTimer {
    
    private static final int MAXIMUM_SIZE = 100000;
    static final int ONE_SECOND = 1000000000;
    private final Method[] methods;

    public MethodsTimer(final Method[] methods) {
        this.methods = methods;
    }
    
    public void report() throws Exception {
        for (Method each : methods) {
            System.out.print(each.getName() + "\t");
            for (int size = 1; size <= MAXIMUM_SIZE; size *= 10) {
                MethodTimer r = new MethodTimer(size, each);
                r.run();
                System.out.print(String.format("%.2f\t", r.getMethodTime()));
            }
            System.out.println();
        }
    }   
}


MethodTimerは以下

package etc9;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class MethodTimer {
    private final int size;
    private final Method method;
    private final Object instance;

    private long totalTime;
    private int iterations;
    private long overhead;

    public MethodTimer(final int size, final Method method) throws Exception {
        this.size = size;
        this.method = method;
        this.instance = createInstance();
    }

    private Object createInstance() throws Exception {
        Constructor<?> constructor = method.getDeclaringClass().getConstructor(
                new Class[] { int.class });
        return constructor.newInstance(new Object[] { size });
    }

    double getMethodTime() {
        return (double) (totalTime - overhead) / (double) iterations;
    }

    void run() throws Exception {
        iterations = 1;
        while (true) {
            totalTime = computeTotalTime();
            if (totalTime > MethodsTimer.ONE_SECOND)
                break;
            iterations *= 2;
        }
        overhead = overheadTimer(iterations).computeTotalTime();
    }

    private long computeTotalTime() throws Exception {
        long start = System.nanoTime();
        for (int i = 0; i < iterations; i++) {
            method.invoke(instance, new Object[0]);
        }
        return System.nanoTime() - start;
    }

    private static MethodTimer overheadTimer(final int iterations)
            throws Exception {
        return new MethodTimer(iterations);
    }

    private MethodTimer(final int iterations) throws Exception {
        this(0, MethodTimer.Overhead.class.getMethod("nothing", new Class[0]));
        this.iterations = iterations;
    }

    public static class Overhead {
        public Overhead(int size) { }
        public void nothing() { }
    }
}

計測対象のクラス

package etc9.target;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;

public class SetVsArrayList {
    private Collection<String> set;
    private Collection<String> arrayList;
    private String probe;

    public SetVsArrayList(final int size) {
        set = new HashSet<String>(size);
        arrayList = new ArrayList<String>(size);
        for (int i = 0; i < size; i++) {
            String element = String.format("a%d", i);
            set.add(element);
            arrayList.add(element);
        }
        probe = String.format("a%d", size / 2);
    }

    public void setMembership() {
        set.contains(probe);
    }
    public void arrayListMembership() {
        arrayList.contains(probe);
    }
}

実行結果は以下のようになり、setはcontaisの処理が要素数の増加により大きな影響を受けないのに対して、arrayListは要素数の増加による影響が大きいことが分かる。

setMembership       64.18  99.68   81.40   111.47    108.80     193.36	
arrayListMembership 60.81 398.99 2257.44 20430.27 207143.09 2206581.18	


MethodTimerのiterationsの使い方がちょっと気になる・・