forループで要素を総なめする処理を書いているときにふと気になったため、collection型の性能比較をしてみました。
対象はよく使うArrayList,HashSetと現在は使用が推奨されていないVectorです。
—
性能比較用コード
public class LoopCompare implements ToysImpl {
private ArrayList<Integer> array = new ArrayList<>();
private HashSet<Integer> hash = new HashSet<>();
private Vector<Integer> vector = new Vector<>();
@Override
public void execute() {
for(int i=0; i<1*1000*1000; i++) {
array.add(i);
hash.add(i);
vector.add(i);
}
System.out.println(
String.format(
“arrray size: %dMiB, hash size: %dMiB, vector size: %dMiB”,
RamUsageEstimator.sizeOf(array) / 1024 /1024,
RamUsageEstimator.sizeOf(hash) / 1024 /1024,
RamUsageEstimator.sizeOf(vector) / 1024 /1024));
WarmUp();
long result = 0; // NOTE デッドコード除去の対象とならないように結果を使用するための変数
long test = System.currentTimeMillis();
result = test(array);
System.out.println(
String.format(
“ArrayList score: %dms, result: %d”,
System.currentTimeMillis() – test,
result));
result = 0;
test = System.currentTimeMillis();
result = test(hash);
System.out.println(
String.format(
“HashSet score: %dms, result: %d”,
System.currentTimeMillis() – test,
result));
result = 0;
test = System.currentTimeMillis();
result = test(vector);
System.out.println(
String.format(
“Vector score: %dms, result: %d”,
System.currentTimeMillis() – test,
result));
}
@Override
public void WarmUp() {
long result = 0; // NOTE デッドコード除去の対象とならないように結果を使用するための変数
for (int i = 1; i <= 10000; i++) {
result += test(array);
result += test(hash);
result += test(vector);
if(i % 1000 == 0) {
result = 0;
System.out.println(“***** ” + (i * 100 / 10000) + “% *****”);
}
}
System.out.println(“warmed ” + result);
}
private long test(Collection<Integer> collection) {
long sum = 0;
for(int i:collection) {
sum += i;
}
return sum;
}
}
実行結果
arrray size: 19MiB, hash size: 53MiB, vector size: 20MiB
ArrayList score: 9ms, result: 499999500000
HashSet score: 11ms, result: 499999500000
Vector score: 27ms, result: 499999500000
—
Vector型は今は推奨されていないだけあって遅いので、ArrayListとHashSetを比較します。
処理速度だけを見ると100万件で数ミリ秒の差しかないためどちらでもよいかなと思いますが、メモリ使用量を考えるとArrayListに軍配が上がりますね。
さて、この結果は何でかというところまでは深掘るつもりはないのでGeminiに聞きます。
まずはメモリ使用量について
> HashSetは、各要素をNodeオブジェクトでラップすることによる要素ごとのオーバーヘッドと、ハッシュテーブルの性質上必要な**「空き」スロット**の両方により、ArrayListやVectorよりも多くのメモリを使用するのです。
> このメモリ消費と引き換えに、HashSetは「要素の重複を許さない」という特性と、「要素の検索(contains)や追加(add)が非常に高速(平均O(1))」という大きな利点を提供します。
処理速度については
> ArrayList:配列のインデックスを順にたどるだけ。最もシンプルで速い。
> HashSet:テーブル全体をスキャンし、各場所(バケット)を調べる必要があり、ArrayListの単純なインデックス順アクセスよりコストがかかる。
> Vector:ループで要素を1つ取得するたびに、ロックの取得・解放という追加のオーバーヘッドが発生し、最も遅くなる。
というわけで今回は要素数が変化しないためArrayListを採用することにしました。
状況に応じてオブジェクトを使い分けるのもオブジェクト指向言語の楽しみかなと思うので、時間に余裕のあるときはドキュメントを読んで似たようなオブジェクトがないか探して性能評価をしてみてください。
(担当:31K)