2009年5月25日月曜日

timeit on java

SpringのAutoWireの有無による速度差と、CGLIBとProxyの速度差を調べたくなった。
AutoWireの有無による速度差はなし。
CGLIBとProxyも十分無視してよい速度差。

上の目的のためにtimeitみたいに、適当な回数分だけループして時間を計測してくれるツールを作った。
一定の時間を越えるまで回数を増やしてループし続けます。
最初は500msにしたら、テストスイートが終わらなくなったので、50ms。
比較用に交互に実行する機能があるといいかも。

public abstract class Timeit {
protected String name;

public Timeit(String name) {
this.name = name;
}

public void timeit() {
int before = 0;
int count = 1;
long sum = 0;
prepare();
while (true) {
int loop = count - before;
long start = System.nanoTime();
for (int i = 0; i < loop; ++i) {
this.invoke();
}
long end = System.nanoTime();
long current = end - start;
sum += current;
if (current > 50 * 1000 * 1000) {
break;
}
before = count;
count = count * 2;
}
long time = sum / count;
System.out.print(name + ": ");
if (time < 1000) {
System.out.printf("%dns\n", time);
} else if (time < 1000 * 1000) {
System.out.printf("%.3fμs %dtimes\n", time / 1000.0, count);
} else if (time < 1000 * 1000 * 1000) {
System.out.printf("%.3fms %dtimes\n", time / 1000.0 / 1000, count);
} else {
System.out.printf("%.3fs %dtimes\n", time / 1000.0 / 1000 / 1000,
count);
}
}

public void prepare() {

}

public abstract void invoke();

public static class TimeItTest extends TestCase {
public void testSimple() throws Exception {
new Timeit("simple") {
public void invoke() {
try {
Thread.sleep(2);
} catch (InterruptedException e) {
}
}
}.timeit();
}
}
}

Mockito

Mockito便利です。
jMockの変態構文に慣れない方は、EasyMockを使ってらっしゃると思います。
EasyMockのスタイルは素敵ですね。
ただ、人に勧めるときに、replayとかverifyとか、定義以外に文をはさむのってなんかいやですよね。
お勧めしづらいですね。
expectLastCallなんかもみっともない。

そこでMockito


// モック作成
@SuppressWarnings("unchecked")
List mockedList = mock(List.class);

// あらかじめ、何が起こるかを書く
when(mockedList.add("one")).thenReturn(true);
// voidはwhenの引数として受け取れないのでdo*で書き直す。
doNothing().when(mockedList).clear();

// モックに対して実行
assertEquals(true, mockedList.add("one"));
mockedList.clear();


事前に定義する方法のほかに、あとからVerifyする方法もあります。

// モック作成
@SuppressWarnings("unchecked")
List mockedList = mock(List.class);

// モックに対して実行
mockedList.add("one");
mockedList.clear();

// 検証
verify(mockedList).add("one");
verify(mockedList).clear();


呼び出し回数も限定しておきます。

@SuppressWarnings("unchecked")
LinkedList mockedList = mock(LinkedList.class);
// モックに対してメソッド呼び出し
mockedList.add("once");

mockedList.add("twice");
mockedList.add("twice");

mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");

// 回数指定で検証開始
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");

// 呼ばれてない確認。times(0)でもOK
verify(mockedList, never()).add("never happened");

// 細かい数は不要。一度でも呼ばれたらOK
verify(mockedList, atLeastOnce()).add("three times");


すでに存在するオブジェクトに対して、部分的にモックを割り当てます。
名づけて"spy"

// 部分的に書き換える。
LinkedList spyList = spy(new LinkedList());
when(spyList.size()).thenReturn(100);

// addやgetは本来の動き
spyList.add("one");
assertEquals("one", spyList.get(0));

// sizeは置き換えました。
assertEquals(100, spyList.size());

verify(spyList).add("one");


Mock作成が楽しくなりました。
Javaの構文による呼び出し順序を利用したマジックですが、
ここまできれいな構文で書けるなら、受け入れるしかないでしょう。
Mock内で引数に加工したり、詳しく検証する場合は、Anythingというインターフェースを使って実装するのですが、そこはまだありがちな構文。

PowerMockもMockitoに対応中。
まあ、PowerMockなんて、一時的に使うことはあっても、すぐ書き直してMockitoだけで通るように変更するのでいらないけど。