本記事の概要
一般的にメモリを大量に使用するCollectionクラスのインスタンスを、拡張ライブラリで実装することで、どの程度メモリ削減できるのかを実測しました。
Collectionクラスの使用メモリサイズ
一般的にCollectionクラスは通常の配列と比較して大量のメモリを使用します。以下のコードでは同じ要素数を持った通常の配列とMapクラスのインスタンスのメモリサイズを比較します。インスタンスの使用メモリの確認方法は以前の記事で説明した方法で行っています。
実行コード
int data_num = 100000;
int[] keys = new int[data_num];
double[] values = new double[data_num];
Map<Integer , Double> map = new HashMap<Integer , Double>();
for(int i = 0 ; i < data_num ; i++){
double tempValue = Math.random();
keys[i] = i;
values[i] = tempValue;
map.put( i , tempValue);
}
System.out.println("keys size(byte) -> " + RamUsageEstimator.sizeOf(keys));
System.out.println("values size(byte) -> " + RamUsageEstimator.sizeOf(values));
System.out.println("map size(byte) -> " + RamUsageEstimator.sizeOf(map));
結果
keys size(byte) -> 400016
values size(byte) -> 800016
map size(byte) -> 8248640
当然の結果ですが、通常の配列と比較してMapクラスのメモリサイズは大変大きいことがわかります。
拡張ライブラリを用いての省メモリ化
Mapクラスの使用メモリを、JavaのCollectionFrameworkを拡張した外部のライブラリを用いて低減させることを考えます。有名なライブラリとして以下があります。
- FastUtil
- Eclipse Collections
- hppc
- Koloboke
- Trove
今回は上の中から、FastUtil と hppc でメモリの削減量を測定します。
ライブラリの導入
FastUtilとhppcはどちらもMavenRepositoryに公開されています。Mavenを使用するプロダクトの場合、以下をpomファイルに追記してください。今回の検証で使用したversionはFastUtilが8.2.1、hppcが0.8.1です。
- FastUtil
<dependency>
<groupId>it.unimi.dsi</groupId>
<artifactId>fastutil</artifactId>
<version>{version}</version>
</dependency>
- hppc
<dependency>
<groupId>com.carrotsearch</groupId>
<artifactId>hppc</artifactId>
<version>{version}</version>
</dependency>
メモリサイズの測定
以下の3つの条件のMapクラスのインスタンスで検証をおこないます。
- Map<Integer , Double> ⇒ keyもvalueもどちらもprimitive型で表現可能なMap
- Map<String , Double> ⇒ keyがObject型、valueがprimitive型で表現可能なMap
- Map<String , String> ⇒ keyもvalueもどちらもObject型でのみ表現可能なMap
以上のMapを通常JDK、FastUtil、hppcの3つで実装して、そのサイズを比較します。data_num=100000とし、それぞれのMapに対し同一なデータをdata_num個収納しています。なおJavaのversionは1.8.0_102を使用しています。
- Map<Int , Double>
実行コード
// JDK
Map<Integer , Double> probabilityMap = new HashMap<Integer, Double>(data_num);
for(int i = 0 ; i < data_num ; i++){
double tempValue = Math.random();
probabilityMap.put(i, tempValue);
}
System.out.println("probabilityMap size(byte) -> " + RamUsageEstimator.sizeOf(probabilityMap));
// hppc
IntDoubleMap hppcMap = new IntDoubleHashMap(data_num);
for(int i = 0 ; i < data_num ; i++){
double tempValue = Math.random();
hppcMap.put(i, tempValue);
}
System.out.println("hppcMap size(byte) -> " + RamUsageEstimator.sizeOf(hppcMap));
// FastUtil
Int2DoubleMap fastUtilMap = new Int2DoubleOpenHashMap(data_num);
for(int i = 0 ; i < data_num ; i++){
double tempValue = Math.random();
fastUtilMap.put(i, tempValue);
}
System.out.println("fastUtilMap size(byte) -> " + RamUsageEstimator.sizeOf(fastUtilMap));
結果
probabilityMap size(byte) -> 8248640
hppcMap size(byte) -> 3145872
fastUtilMap size(byte) -> 3145848
- Map<String , Double>
実行コード
// JDK
Map<String , Double> probabilityMap = new HashMap<String, Double>(data_num);
for(int i = 0 ; i < data_num ; i++){
double tempValue = Math.random();
probabilityMap.put(String.valueOf(i), tempValue);
}
System.out.println("probabilityMap size(byte) -> " + RamUsageEstimator.sizeOf(probabilityMap));
// hppc
ObjectDoubleMap<String> hppcMap = new ObjectDoubleHashMap<String>(data_num);
for(int i = 0 ; i < data_num ; i++){
double tempValue = Math.random();
hppcMap.put(String.valueOf(i), tempValue);
}
System.out.println("hppcMap size(byte) -> " + RamUsageEstimator.sizeOf(hppcMap));
// FastUtil
Object2DoubleMap<String> fastUtilMap = new Object2DoubleOpenHashMap<String>(data_num);
for(int i = 0 ; i < data_num ; i++){
double tempValue = Math.random();
fastUtilMap.put(String.valueOf(i), tempValue);
}
System.out.println("fastUtilMap size(byte) -> " + RamUsageEstimator.sizeOf(fastUtilMap));
結果
probabilityMap size(byte) -> 12168640
hppcMap size(byte) -> 8665872
fastUtilMap size(byte) -> 8665848
- Map<String , String>
実行コード
// JDK
Map<String , String> probabilityMap = new HashMap<String, String>(data_num);
for(int i = 0 ; i < data_num ; i++){
probabilityMap.put(String.valueOf(i), String.valueOf(i));
}
System.out.println("probabilityMap size(byte) -> " + RamUsageEstimator.sizeOf(probabilityMap));
// hppc
ObjectObjectMap<String,String> hppcMap = new ObjectObjectHashMap<String,String>(data_num);
for(int i = 0 ; i < data_num ; i++){
hppcMap.put(String.valueOf(i), String.valueOf(i));
}
System.out.println("hppcMap size(byte) -> " + RamUsageEstimator.sizeOf(hppcMap));
// FastUtil
Object2ObjectMap<String , String> fastUtilMap = new Object2ObjectOpenHashMap<String , String>(data_num);
for(int i = 0 ; i < data_num ; i++){
fastUtilMap.put(String.valueOf(i), String.valueOf(i));
}
System.out.println("fastUtilMap size(byte) -> " + RamUsageEstimator.sizeOf(fastUtilMap));
結果
probabilityMap size(byte) -> 15288640
hppcMap size(byte) -> 13137296
fastUtilMap size(byte) -> 13137264
結果まとめと考察
結果としては、
- 全ての条件でJDKを使用するよりも拡張ライブラリを使用した方が使用メモリが低下した。
- 低下率はprimitive型を多く使用している方が高くなる。
- hppcとFastUtilではほとんどメモリサイズは変わらないが、FastUtilが少しだけ小さい。
となりました。
JDKではラッパー型で実装されているコレクションクラスを、各primitive型用に書き直すことで高効率に処理しているのが拡張ライブラリの一番の特徴です。 そのため、primitive型を含むMapの方が高いメモリ削減を行えたと考えられます。ただし、オブジェクト型のみのMapであってもメモリ使用量が低下しているので、その他の方法も取り入れ総合的にメモリの削減を行っているようです。同じ拡張ライブラリでもhppcと比較してFastUtilの方が更にメモリの削減を行っています。
拡張ライブラリ使用のすすめ
上記のように拡張ライブラリを使用することでメモリの削減ができますが、その他にも拡張ライブラリは通常のコレクションクラスと比較して、一般的に次の良い特性を持っています。
- primitive型のgetやput等の処理の速度が速い。
- インスタンスのGCの速度が速い。
- 便利な関数が追加されている。
もちろんデータの内容や環境に依る部分もあるので、自分の環境でそれぞれの拡張ライブラリのベンチマークをとりつつ導入検討することをお勧めします。