Java Day 2018のセッション『Performance tuning with poor tools and cheap drink』に、「HashMapのキーとして、複数の文字列を連結(firstName + familyName)することより、複合キークラスを作成したほうがパフォーマンスがよい」というテクニックを紹介されたので、本当にそうであろうか検証してみました。
検証内容
検証は以下の内容で実施しました。
- 連結キーマップ:文字列と数値を連結した文字列をキーとする
- 複合キーマップ:文字列、数値をメンバー変数とした複合キークラスをキーとする
- それぞれのマップに対し、putとgetをそれぞれ2,000,000回で実施し、トータル処理時間を計測
- また、マップの作成として、初期容量、負荷係数ともデフォルト(容量の75%になる度にマップが拡張される)のパターンと初期容量が2,000,001で、負荷係数が1(マップの拡張を抑制)のパターンとそれぞれ計測
検討結果
以下は検証結果(単位:ms)となります。
複合キーのhashCodeをInteger.valueOf(index).hashCode()とした場合:
■連結キーマップ(初期容量、負荷係数ともデフォールト)
putComplexKeyMap execution time: 1060
getComplexKeyMap execution time: 312
■連結キーマップ(初期容量:2000001、負荷係数:1.0)
putComplexKeyMap execution time: 303
getComplexKeyMap execution time: 154
■複合キーマップ(初期容量、負荷係数ともデフォールト)
putCompositeKeyMap execution time: 256
複合キーマップキーのequalsメソッド呼び出す回数(put時): 0
key hashcode: 0 value: 0
key hashcode: 1 value: 1
getCompositeKeyMap execution time: 762
複合キーマップキーのequalsメソッド呼び出す回数(get時): 2000000
■複合キーマップ(初期容量:2000001、負荷係数:1.0)
putCompositeKeyMap execution time: 659
複合キーマップキーのequalsメソッド呼び出す回数(put時): 0
key hashcode: 0 value: 0
key hashcode: 1 value: 1
getCompositeKeyMap execution time: 198
複合キーマップキーのequalsメソッド呼び出す回数(get時): 2000000
初期容量、負荷係数ともデフォールトの場合のput処理は複合キーのほうが速かったが、それ以外は連結キーのほうが速いでした。(鵜呑みせずに自分で確かめることも大事ではなかろうか)
複合キーのhashCodeを(prefix + index).hashCode()とした場合:
■連結キーマップ(初期容量、負荷係数ともデフォールト)
putComplexKeyMap execution time: 1071
getComplexKeyMap execution time: 307
■連結キーマップ(初期容量:2000001、負荷係数:1.0)
putComplexKeyMap execution time: 313
getComplexKeyMap execution time: 183
■複合キーマップ(初期容量、負荷係数ともデフォールト)
putCompositeKeyMap execution time: 1483
複合キーマップキーのequalsメソッド呼び出す回数(put時): 0
key hashcode: 3055 value: 0
key hashcode: 3056 value: 1
getCompositeKeyMap execution time: 423
複合キーマップキーのequalsメソッド呼び出す回数(get時): 2000000
■複合キーマップ(初期容量:2000001、負荷係数:1.0)
putCompositeKeyMap execution time: 756
複合キーマップキーのequalsメソッド呼び出す回数(put時): 0
key hashcode: 3055 value: 0
key hashcode: 3056 value: 1
getCompositeKeyMap execution time: 577
複合キーマップキーのequalsメソッド呼び出す回数(get時): 2000000
put処理、get処理も連結キーのほうが速いでした。
複合キーのhashCodeをObjects.hash(prefix, index)とした場合:
■連結キーマップ(初期容量、負荷係数ともデフォールト)
putComplexKeyMap execution time: 1086
getComplexKeyMap execution time: 323
■連結キーマップ(初期容量:2000001、負荷係数:1.0)
putComplexKeyMap execution time: 283
getComplexKeyMap execution time: 182
■複合キーマップ(初期容量、負荷係数ともデフォールト)
putCompositeKeyMap execution time: 913
複合キーマップキーのequalsメソッド呼び出す回数(put時): 0
key hashcode: 3968 value: 0
key hashcode: 3969 value: 1
getCompositeKeyMap execution time: 401
複合キーマップキーのequalsメソッド呼び出す回数(get時): 2000000
■複合キーマップ(初期容量:2000001、負荷係数:1.0)
putCompositeKeyMap execution time: 441
複合キーマップキーのequalsメソッド呼び出す回数(put時): 0
key hashcode: 3968 value: 0
key hashcode: 3969 value: 1
getCompositeKeyMap execution time: 276
複合キーマップキーのequalsメソッド呼び出す回数(get時): 2000000
結果まちまちですが、それほど差が大きくなかった。
hashCode次第で性能への影響が大きいですね。
また、検証結果から以下のこともわかりました。
- 最初からマップの容量が分かれれば、マップの拡張を抑止することで大幅に性能アップ
- 複合キーを利用する場合、hashCode、equalsメソッドともに独自実装する必要があり、またその場合、equalsメソッドの呼び出す回数も多い
- 初期容量、負荷係数はマップの拡張には影響があるが、キーのequalsメソッド呼び出す回数には影響が軽微
検証プログラム
PerformanceHelper.java
package test.pfm;
import java.util.function.Consumer;
public final class PerformanceHelper {
/**
* 指定した回数で実行対象メソッドを実行し、総処理時間(ms)を返却。
* @param times 実行回数
* @param consumer 実行対象メソッド
* @return 総処理時間
*/
public static long getMethodExecutionTime(int times, Consumer<Integer> consumer) {
long start = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
consumer.accept(i);
}
long end = System.currentTimeMillis();
return end- start;
}
/**
* 指定した回数で実行対象メソッドを実行し、総処理時間(ms)を出力する。
* @param times 実行回数
* @param consumer 実行対象メソッドを代理するConsumer
* @param methodName 実行対象メソッド名またはガイドメッセージ
* @return 総処理時間
*/
public static void printMethodExecutionTime(int times, Consumer<Integer> consumer, String methodName) {
System.out.println(methodName + " execution time: " + getMethodExecutionTime(times, consumer));
}
}
MapPerformanceTest.java
package test.pfm;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class MapPerformanceTest {
private static final int REPEAT_TIMES = 2000000;
private static final float MAP_LOAD_FACTOR = 1;
private static int compositeKeyEqualsInvokeCount;
private static Map<String, String> complexKeyMap;
private static Map<CompositeKey, String> compositeKeyMap;
public static void main(String[] args) {
System.out.println("■連結キーマップ(初期容量、負荷係数ともデフォールト)");
complexKeyMap = new HashMap<>();
PerformanceHelper.printMethodExecutionTime(REPEAT_TIMES, (index) -> putComplexKeyMap(index),
"putComplexKeyMap");
PerformanceHelper.printMethodExecutionTime(REPEAT_TIMES, MapPerformanceTest::getComplexKeyMap,
"getComplexKeyMap");
System.out.println("");
System.out.println("■連結キーマップ(初期容量:" + (REPEAT_TIMES + 1) + "、負荷係数:" + MAP_LOAD_FACTOR + ")");
complexKeyMap = new HashMap<>(REPEAT_TIMES + 1, MAP_LOAD_FACTOR);
PerformanceHelper.printMethodExecutionTime(REPEAT_TIMES, (index) -> putComplexKeyMap(index),
"putComplexKeyMap");
PerformanceHelper.printMethodExecutionTime(REPEAT_TIMES, MapPerformanceTest::getComplexKeyMap,
"getComplexKeyMap");
System.out.println("");
System.out.println("■複合キーマップ(初期容量、負荷係数ともデフォールト)");
compositeKeyMap = new HashMap<>();
PerformanceHelper.printMethodExecutionTime(REPEAT_TIMES, (index) -> putCompositeKeyMap(index),
"putCompositeKeyMap");
System.out.println("複合キーマップキーのequalsメソッド呼び出す回数(put時): " + compositeKeyEqualsInvokeCount);
compositeKeyEqualsInvokeCount = 0;
PerformanceHelper.printMethodExecutionTime(REPEAT_TIMES, MapPerformanceTest::getCompositeKeyMap,
"getCompositeKeyMap");
System.out.println("複合キーマップキーのequalsメソッド呼び出す回数(get時): " + compositeKeyEqualsInvokeCount);
System.out.println("");
System.out.println("■複合キーマップ(初期容量:" + (REPEAT_TIMES + 1) + "、負荷係数:" + MAP_LOAD_FACTOR + ")");
compositeKeyEqualsInvokeCount = 0;
compositeKeyMap = new HashMap<>(REPEAT_TIMES + 1, MAP_LOAD_FACTOR);
PerformanceHelper.printMethodExecutionTime(REPEAT_TIMES, (index) -> putCompositeKeyMap(index),
"putCompositeKeyMap");
System.out.println("複合キーマップキーのequalsメソッド呼び出す回数(put時): " + compositeKeyEqualsInvokeCount);
compositeKeyEqualsInvokeCount = 0;
PerformanceHelper.printMethodExecutionTime(REPEAT_TIMES, MapPerformanceTest::getCompositeKeyMap,
"getCompositeKeyMap");
System.out.println("複合キーマップキーのequalsメソッド呼び出す回数(get時): " + compositeKeyEqualsInvokeCount);
}
private MapPerformanceTest(int mapCapacity) {
compositeKeyMap = new HashMap<>(mapCapacity, 0.75f);
compositeKeyEqualsInvokeCount = 0;
}
private static void putComplexKeyMap(int index) {
complexKeyMap.put("a" + index, String.valueOf(index));
}
private static String getComplexKeyMap(int index) {
return complexKeyMap.get("a" + index);
}
private static void putCompositeKeyMap(int index) {
compositeKeyMap.put(new CompositeKey("a", index), String.valueOf(index));
}
private static String getCompositeKeyMap(int index) {
CompositeKey key = new CompositeKey("a", index);
String value = compositeKeyMap.get(key);
if (index < 2) {
System.out.println("key hashcode: " + key.hashCode() + " value: " + value);
}
return value;
}
private static class CompositeKey {
private String prefix;
private int index;
CompositeKey(String prefix, int index) {
this.prefix = prefix;
this.index = index;
}
@Override
public int hashCode() {
// return Integer.valueOf(index).hashCode();
// return (prefix + index).hashCode();
return Objects.hash(prefix, index);
}
@Override
public boolean equals(Object obj) {
compositeKeyEqualsInvokeCount++;
CompositeKey key = (CompositeKey) obj;
if ((index == key.getIndex()) && prefix.equals(key.getPrefix())) {
return true;
}
return false;
}
String getPrefix() {
return prefix;
}
int getIndex() {
return index;
}
}
}
おまけ
検証プログラムにはJavaの以下のテクニックを利用しました。まだ詳しくない方はご参考いただきたい。
- アクセススコープ修飾子、finalキーワードの使い分け
- メソッド参照
- ObjectクラスのhashCode、equalsメソッドのオーバーライド
- FunctionalInterfaceのConsumerの実装
- ラムダ式
周@ソフトシンク