HashMapを極め：連結キーVS複合キー

[Java Day 2018](https://qiita.com/SoftThink/items/288ce63500785cb6a4a0)のセッション『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メソッド呼び出す回数には影響が軽微

#検証プログラム
```java: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));
    }
}
```

```java: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の実装
* ラムダ式

周@ソフトシンク