S-JIS[2009-03-04/2009-03-25] 変更履歴

Javaのコスト

コストと一言で言ってもいろいろな意味があるが、ここでは実行時のコストについて考えてみる。

テスト用ソース: cost.src.zip


基本的な処理にかかる時間

インスタンス生成(new)やthrow-catchといった基本操作にかかる時間を調べてみた。(WindowsXPEclipse3.4[2009-03-05]

new 100万回の実行時間[ms]
クラス JRE1.4.2 JRE1.5.0 JRE1.6.0
new Object() 55.2 8.2 6.7
new String() 122 41 36
new StringBuilder() - 60 55
new StringBuffer() 146 63 56
new ArrayList() 180 87 78
new HashMap() 397 234 109
new Throwable() 2336 2175 1155
new Exception() 2400 2208 1160
new RuntimeException() 2500 2227 1197
new Thread() 4700 3100 1600
new SimpleDateFormat() 23500 18700 18700

インスタンス生成と一口に言っても、クラスによって全然速度が違う。(コンストラクターの内容次第だろうから、当たり前だけど)
でもJDK1.6ではけっこう高速化が図られているようだ。

100万回の実行時間[ms]
  ステートメント JRE1.4.2 JRE1.5.0 JRE1.6.0
2009-03-05 空ループ 0.76 1.09 1.10
2009-03-05 throw-catch 187〜204 187〜203 109〜110
2009-03-05 synchronized 113 107 120
2009-03-07 insntaceof C(マッチ) 3.1 3.3 3.1
2009-03-07 insntaceof C(アンマッチ) 3.1 3.3 3.1
2009-03-07 insntaceof I(マッチ) 2.5 3.2 3.0
2009-03-07 insntaceof I(アンマッチ) 32.1 32.6 32.7
//throw-catch
	Exception t = new RuntimeException();
	try { throw t; }catch(Exception e){}		←これをループ
//synchronized
	synchronized(クラス.class) { }		←これをループ

try〜catchはJDK1.6で速くなっている。
逆に空ループはJDK1.4→JDK1.5で微妙に遅くなっている。とは言っても100万回で1ミリ秒未満なら、他の実行速度に比べると無視できる話だ。
synchronizedはJDK1.5で速くなったのに、JDK1.6で遅くなっている。何が変わったんだろうなぁ?


synchronizedは確かに無処理に比べれば遅いが、インスタンス生成と比べれば、文字列結合2〜3個分と同じくらい?
もちろんマルチスレッドの競合の頻度にもよるだろうけど、そんなに目くじら立てるほど遅くない気がする。


instanceofは、JDKのバージョンによる速度の違いはあまり無いようだ。[2009-03-07]

それにしても、
「obj instanceof クラス」は、objがそのクラスのインスタンスであっても無くてもほとんど速度の差は無いが、
「obj instanceof インターフェース」は、objがそのインターフェースを実装しているかどうかで10倍くらい速度が違う!
(その遅い場合でも、StringやStringBuilderのインスタンス生成よりは速いけど)

インターフェースを実装している場合はすぐ結論が出るけれど、実装していない場合は調べるものが多いから遅いのだろうか。
・全ての親クラスが該当インターフェースを実装しているかどうか調べないといけない
・実装している全インターフェースが該当インターフェースを継承しているかどうかも調べないといけない

100万回の実行時間[ms](JRE1.6.0)
 
クラス c = (クラス)obj;
if (obj instanceof クラス) {
  クラス c = (クラス)obj;
}
try {
  クラス c = (クラス)obj;
} catch (ClassCastException e) {}
マッチする場合 4.0 5.6 5.1
マッチしない場合 (例外発生) 4.2 8577

「作っているプログラムの仕様上、オブジェクトが必ずそのクラスにキャストできる」と分かっているなら、ただ単にキャストすればいい。
クラスが違う可能性もあるなら、instanceofもペアにして使う。
この実行速度なら、違う可能性が低い場合はClassCastExceptionをキャッチするコーディングも有り…なのかなぁ?
違う可能性が高いのにClassCastExceptionをキャッチして続行するようなプログラムは、かなり(1000倍以上)効率悪そう。


contains()の速度

値の一覧の中から、ある値が存在しているかどうかを調べるには、Map#containskey()(Set#contains())を使う。[2009-03-04]
が、ハッシュマップはハッシュ値を計算したりする手間があるので、個数が少ないならList#contains()の方が速いんじゃないかと思ってちょっと速度を測ってみた。(WindowsXPEclipse3.4

なお、値一覧は固定という想定で、コレクション(MapやList)の生成や変更のコストは考慮に含めない。

下記の表の中の各バージョン毎の数値は、左から順に
Arrays#binarySearch()、ArrayList#contains()、HashMap#containsKey()

キーの全候補で探索(500万回の実行時間[ms])
キー 要素数 JRE1.4.2 JRE1.5.0 JRE1.6.0
配列 List Map 配列 List Map 配列 List Map
String 1桁 0 0 15 16 15 16 16 0 16 16
1 375 359 454 265 235 281 272 219 250
2 922 781 860 656 547 531 750 510 510
3 1438 1484 1328 1094 1109 859 1281 1016 766
String 5桁 0 0 16 16 16 16 16 0 15 16
1 360 350 469 265 219 282 344 234 242
2 906 813 859 750 610 531 1000 640 485
3 1500 1657 1328 1375 1360 859 1656 1391 782
String 6桁 0 0 15 16 0 15 16 0 15 16
1 344 359 462 266 234 281 360 228 250
2 906 891 844 766 687 546 1046 641 500
3 1469 1844 1312 1406 1672 859 1734 1453 781
Integer 0 0 15 16 0 16 15 0 16 16
1 344 359 410 235 247 291 219 221 313
2 782 781 812 563 547 578 547 515 610
3 1265 1500 1281 1062 1078 875 1016 1046 954
4 1812 2218 1641 1609 1594 1187 1557 1547 1250

「コレクション内の個数(要素数)が少ないならHashMap#containsKey()よりArrayList#contains()の方が速い」ということ自体は予想通りだけれども、その「少ないなら」の具体的な数値は「1個まで」か!(爆)
キーがStringの場合、JDK1.4では少なくとも要素数3個でHashMapの方が速くなるが、JDK1.5以降は要素数2個で速くなる。
また、JDK1.4の場合はArrayListに対するStringの桁数の影響も大きく、String6桁だと要素数2個でHashMapの方が速くなる。

結局のところ、equals()での文字列比較は文字数の回数だけ比較を行うので、ハッシュ値の計算を先に行っても比較にそのハッシュ値を使う方が速くなるのだろう。
Integerの場合はequals()は単純なので、要素数2個でもArrayListの方がHashMapより速いのだろう。

なお、リストの先頭でマッチするキーで検索をかける場合は、要素数に関わらずArrayListの方がHashMapより速い。
とは言っても、どんなに要素数が多くなっても HashMapはArrayListより一定時間遅いだけなので、優秀だ。
逆にリストの末尾でマッチするキーで検索をかけると、ArrayListは要素数が多いほど比例して時間がかかる(リストとは当然そういうものだ)のに対し、HashMapはもっと短時間(ほぼ一定時間(厳密にはオーダーの…いくつだったっけ?(爆)))で終わる(それでこそマップだ)

それにしても、JREのバージョンによってかなり速度が違うのに驚き。JDK1.4のこの遅さは何なんだ(苦笑)
String・Integerを扱うArrayList・HashMapの両方共、JDK1.4よりJDK1.5以降の方がずっと高速。
Stringの場合は、ArrayListもHashMapもJDK1.6でさらにJDK1.5より高速になってる。
逆にIntegerをキーとして扱うのは、HashMapはJDK1.6で少し遅くなっているようだが…ArrayListは若干速い。

結論

変化しない一覧を保持して存在有無をチェックするような場合、HashMap#containsKey()(HashSet#contains())を使うのがいい。
(そういう使い方をする場合、要素数が2個なんてことはまず有り得ないので。)


ソートの速度

データ一覧(比較用の値が重複しない)をソートする必要がある場合、一旦リストに入れてからCollections#sort()を呼ぶ方法と、TreeSetを使ってソートしながらデータ一覧を作成する方法が考えられる。[2009-03-25]
(重複するデータがある場合、TreeSetは使えない。Collections#sort()は重複する箇所は元のソート順が保証される)

  Collections#sort() TreeSet
速い 遅い 速い 遅い
元となるデータ
List data = new ArrayList();
data.add(〜);
インスタンス生成
List list = new ArrayList(data);
int size = data.size();
List list = new ArrayList(size);
for (int j = 0; j < size; j++) {
  list.add(data.get(j));
}
int size = data.size();
Set list = new TreeSet();
for (int j = 0; j < size; j++) {
  list.add(data.get(j));
}
Set list = new TreeSet(data);
ArrayListはコンストラクターにコレクションを渡す方が速い。
内部でコレクションのtoArray()を使う為。
TreeSetは1つずつ入れる方が速い。
コレクションを渡す方式では内部でIteratorパターンを使う為。
Listループの速度比較
つまり最初のコレクションがRandomAccessでない場合は、
コンストラクターにコレクションを渡す方式と同じ速度になるだろう。
ソート
Collections.sort(list);
-
データ出力
int size = list.size();
for (int j = 0; j < size; j++) {
   Object obj = list.get(j);
// System.out.println(obj);
}
for (Iterator j = list.iterator();
 j.hasNext();) {
   Object obj = j.next();
// System.out.println(obj);
}
for (Iterator j = list.iterator();
 j.hasNext();) {
   Object obj = j.next();
// System.out.println(obj);
}

データは別のArrayListを用意し、そこからソート用のArrayList/TreeSetを作り、ループしてデータを1つずつ取り出す一連の処理を行う。
ソート用のインスタンス生成からデータを取り出すまでの時間を計測した。

インスタンス生成も「コンストラクターにデータのArrayListを直接渡す方式」と「デフォルトインスタンスだけ作って自分でデータを追加していく方式」の二種類考えられるが、ArrayListとTreeSetでは、それぞれ速い方式が違う。TreeSetは速い方だけ測定した。
ソート後のデータ出力は、下記のsort(速)もsort(遅)もRandomAccess方式(速い方)を使用した。

ソート(100万回の実行時間[ms](誤差:少なくとも±50))
元データ 要素数 JRE1.4.2 JRE1.5.0 JRE1.6.0
sort(速) sort(遅) TreeSet sort(速) sort(遅) TreeSet sort(速) sort(遅) TreeSet
String 4桁
(交互順)
3 2180 2240 2250 1630 1640 1200 1390 1600 970
6 3130 3430 3670 2360 2550 2270 2080 2560 1880
9 4360 4890 5220 3330 3630 3520 3000 3690 2910
12 5250 5880 6880 3970 4380 4750 3600 4500 3910
30 12580 14610 17580 9770 11170 13200 9220 11720 10700
String 4桁
(ソート済)
3 2030 2140 2200 1480 1520 1260 1320 1520 970
6 2780 3100 3550 2030 2240 2330 1830 2330 1800
9 3630 4240 5110 2640 3050 3720 2490 3220 2940
12 4380 5250 6750 3160 3820 5160 3070 4030 4100
30 9920 12270 18200 7420 9220 15160 7110 9840 11880
String 4桁
(逆順で
ソート済)
3 2140 2240 2250 1600 1620 1260 1400 1600 970
6 3570 3900 3520 2770 2990 2350 2430 3030 1820
9 4630 5270 5110 3520 3890 3720 3300 3940 2940
12 6250 7100 6820 4690 5280 5290 4600 5410 4100
30 14380 8330 16800 11170 12820 15310 10470 13200 11880

JDK1.4では、sort(速)(ArrayListをコンストラクターから作る方式のCollections#sort())が明らかに速い。
ただし、実際の使用用途から言って、ArrayListからArrayListを作り直すことはたぶんあまり無いので、sort(遅)の方が普通に出てくるパターンかもしれない。(ただし、元データがArrayList以外でも、自分で1個ずつadd()するよりはコンストラクターに任せる方が速い可能性はある)
それでも、JDK1.4ではsort()の方がTreeSetより(ほぼ全パターンにわたって)速いが。

JDK1.5や1.6では、データ数が少ない場合はTreeSetの方が速い。
JDK1.5では、データ数が10個程度以上なら、Collections#sort()を使う方が速そうだ。
JDK1.6ではTreeSetがだいぶ優秀になっているみたいなので、どれくらいの個数で見切るか(sort()とTreeSetのどちらを使うか)を判断するのはちょっと難しそう。


Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま