web-dev-qa-db-ja.com

プリミティブ値の代替マップ

アプリケーションでプロファイリングを行った結果、ヒープ上のメモリの約18%がDouble型のオブジェクトによって使用されていることが判明しました。これらのオブジェクトは、プリミティブ型を使用できないMapsの値であることがわかります。

私の推論では、doubleのプリミティブ型は、オブジェクトDoubleよりも少ないメモリを消費します。キーとして任意の型を受け入れ、値としてプリミティブdoubleを受け入れるデータ構造のようなマップを作成する方法はありますか?

主な操作は次のとおりです。

  • 挿入(おそらく1回のみ)
  • ルックアップ(キーごとに含まれる)
  • 検索(キーによる)
  • 反復

私が持っている典型的なマップは次のとおりです。

  • HashMap<T, HashMap<NodeData<T>, Double>> graph
  • HashMap<Point2D, Boolean> onSea(ただし、二重の値ではありません)
  • ConcurrentHashMap<Point2D, HashMap<Point2D, Double>>

Java 8。

補遺

私は主に、これらのタイプのマップに解決策があるフレームワークには興味がありませんが、これらの問題に対処する際に考慮しなければならないものに興味があります。必要に応じて、そのようなフレームワークの背後にある概念/アイデア/アプローチは何ですか。または、ソリューションが別のレベルにあり、マップが、@ Ilmari Karonenが答えで指摘したような特定のパターンに従ってオブジェクトに置き換えられる場合もあります。

22
hotzst

他の人は、プリミティブ値マップのいくつかのサードパーティ実装をすでに提案しています。完全を期すために、マップを完全に取り除くいくつかの方法について言及したいと思います。これらのソリューションは常に可能とは限りませんが、可能であれば、どのマップよりも高速でメモリ効率がよくなります。

代替方法1:単純な古い配列を使用します。

単純なdouble[]配列は派手なマップほどセクシーではないかもしれませんが、コンパクトでアクセスの速度が非常に小さいです。

もちろん、配列には多くの制限があります:サイズは固定されています(ただし、新しい配列を作成して古いコンテンツをそこにコピーすることはいつでも可能です)。また、キーは小さな正の整数にしかできません。密(つまり、使用されるキーの総数は、最高のキー値のかなり大きな割合である必要があります)。しかし、それがキーに当てはまる場合、またはキーに当てはまる場合、プリミティブ値の配列は非常に効率的です。

特に、一意の短整数IDを各キーオブジェクトに割り当てることができる場合、そのIDを配列のインデックスとして使用できます。同様に、オブジェクトを既に配列に格納し(たとえば、より複雑なデータ構造の一部として)、インデックスで検索している場合、同じインデックスを使用して別の配列の追加のメタデータ値を検索できます。

ある種の衝突処理メカニズムを実装した場合は、IDの一意性要件を省くこともできますが、その時点で独自のハッシュテーブルの実装に向けて順調に進んでいます。場合によってはmightが実際に理にかなっていますが、通常はその時点で既存のサードパーティの実装を使用する方がおそらく簡単です。

代替方法2:オブジェクトをカスタマイズします。

キーオブジェクトからプリミティブ値へのマップを維持する代わりに、それらの値をオブジェクト自体のプロパティにするだけではどうですか?結局のところ、これはオブジェクト指向プログラミングのすべてであり、関連するデータを意味のあるオブジェクトにグループ化します。

たとえば、HashMap<Point2D, Boolean> onSeaを維持する代わりに、単にポイントにブール値のonSeaプロパティを与えないのはなぜですか?もちろん、これには独自のカスタムポイントクラスを定義する必要がありますが、必要に応じて標準のPoint2Dクラスを拡張して、カスタムポイントを渡すことができない理由はありません。 Point2Dを期待するメソッド。

繰り返しますが、このアプローチは常に直接機能するとは限りません。変更できないクラスを操作する必要がある場合(ただし、以下を参照)、または保存する値が複数のオブジェクトに関連付けられている場合(ConcurrentHashMap<Point2D, HashMap<Point2D, Double>>のように)。

ただし、後者の場合は、データ表現を適切に再設計することで問題を解決できる場合があります。たとえば、加重グラフをMap<Node, Map<Node, Double>>として表す代わりに、次のようなEdgeクラスを定義できます。

class Edge {
    Node a, b;
    double weight;
}

そして、そのノードに接続されたエッジを含む各ノードにEdge[](またはVector<Edge>)プロパティを追加します。

選択肢3:複数のマップを1つに結合します。

同じキーを持つ複数のマップがあり、上記のように値をキーオブジェクトの新しいプロパティに変換できない場合は、それらを単一のメタデータクラスにグループ化し、キーからそのクラスのオブジェクトへの単一のマップを作成することを検討してください。たとえば、Map<Item, Double> accessFrequencyおよびMap<Item, Long> creationTimeの代わりに、次のような単一のメタデータクラスを定義することを検討してください。

class ItemMetadata {
    double accessFrequency;
    long creationTime;
}

すべてのメタデータ値を格納する単一のMap<Item, ItemMetadata>を持ちます。これは、複数のマップを使用するよりもメモリ効率が高く、冗長なマップルックアップを回避することで時間を節約することもできます。

場合によっては、便宜上、各メタデータオブジェクトに対応するメインオブジェクトへの参照を含めて、メタデータオブジェクトへの単一の参照を通じて両方にアクセスできるようにすることもできます。自然にセグメンテーション...

代替方法4:デコレータを使用します。

前の2つの選択肢の組み合わせとして、キーオブジェクトに追加のメタデータプロパティを直接追加できない場合は、代わりに追加の値を保持できる decorators でラップすることを検討してください。したがって、たとえば、追加のプロパティを使用して独自のポイントクラスを直接作成する代わりに、次のようなことができます。

class PointWrapper {
    Point2D point;
    boolean onSea;
    // ...
}

必要に応じて、メソッドフォワーディングを実装することにより、このラッパーを本格的なデコレータに変えることもできますが、多くの目的には単純な「ダム」ラッパーで十分です。

このアプローチは、ラッパーのみを格納および操作するように調整できるため、ラップされていないオブジェクトに対応するラッパーを検索する必要がない場合に最も役立ちます。もちろん、たまにそれを行う必要がある場合(たとえば、他のコードからラップされていないオブジェクトのみを受け取るため)、1つのMap<Point2D, PointWrapper>でそれを行うことができますが、以前の代替。

10
Ilmari Karonen

Eclipse Collections には object および primitive maps があり、両方にMutableおよびImmutableバージョンがあります。

MutableObjectDoubleMap<String> doubleMap = ObjectDoubleMaps.mutable.empty();
doubleMap.put("1", 1.0d);
doubleMap.put("2", 2.0d);

MutableObjectBooleanMap<String> booleanMap = ObjectBooleanMaps.mutable.empty();
booleanMap.put("ok", true);

ImmutableObjectDoubleMap<String> immutableMap = doubleMap.toImmutable();
Assert.assertEquals(doubleMap, immutableMap);

MutableMapは、上記の例で行ったようにImmutableMapを呼び出すことで、Eclipse CollectionsのtoImmutableのファクトリとして使用できます。可変マップと不変マップは、共通の親インターフェースを共有します。親インターフェースは、上記のMutableObjectDoubleMapImmutableObjectDoubleMapの場合、ObjectDoubleMapという名前になります。

また、Eclipse Collectionsには、ライブラリ内のすべての可変コンテナ用に同期された変更不可能なバージョンがあります。次のコードは、プリミティブマップをラップした同期ビューを提供します。

MutableObjectDoubleMap<String> doubleMap = 
        ObjectDoubleMaps.mutable.<String>empty().asSynchronized();
doubleMap.put("1", 1.0d);
doubleMap.put("2", 2.0d);

MutableObjectBooleanMap<String> booleanMap = 
        ObjectBooleanMaps.mutable.<String>empty().asSynchronized();
booleanMap.put("ok", true);

この大きなマップのパフォーマンス比較は、数年前に公開されました。

大規模なHashMapの概要:JDK、FastUtil、Goldman Sachs、HPPC、Koloboke、Trove – 2015年1月バージョン

GS Collectionsはその後Eclipse Foundationに移行され、現在はEclipse Collectionsになっています。

注:私はEclipse Collectionsのコミッターです。

15
Donald Raab

探しているのは _Object2DoubleOpenHashMap_ from fastutil (Collections Framework withタイプ double getDouble(Object k) および double put(K k, double v) のメソッドを提供する小さなメモリフットプリントと高速アクセスおよび挿入)。

例えば:

_// Create a Object2DoubleOpenHashMap instance
Object2DoubleMap<String> map = new Object2DoubleOpenHashMap<>();
// Put a new entry
map.put("foo", 12.50d);
// Access to the entry
double value = map.getDouble("foo");
_

クラス_Object2DoubleOpenHashMap_は、スレッドセーフではないMapの実際の実装ですが、ユーティリティメソッド Object2DoubleMaps.synchronize(Object2DoubleMap<K> m) を使用して作成できます。デコレータのおかげでスレッドセーフです。

作成コードは次のようになります。

_// Create a thread safe Object2DoubleMap
Object2DoubleMap<String> map =  Object2DoubleMaps.synchronize(
    new Object2DoubleOpenHashMap<>()
);
_
13
Nicolas Filotto

いくつかの実装があります。

最高のパフォーマンスに関連する質問を次に示します。

実際の実装もパフォーマンスに影響を与える可能性があります

2
ppasler

これらのさまざまなライブラリが互いにどのようにスタックするかをより正確に推定するために、以下のパフォーマンスをチェックする小さなベンチマークをまとめました。

  • 300'000の挿入の合計時間
  • マップにある1000個のサンプルを含む封じ込めのチェックの平均時間
  • データ構造のメモリサイズMapのような構造を見て、キーとしてStringを、値としてdoubleを取りました。チェックされているフレームワークは、 Eclipse Collection[〜#〜] hppc [〜#〜]Trove および FastUtil 、および比較用HashMapおよびConcurrentHashMap

要するに、これらは結果です:

Filling in 300000 into the JDK HashMap took 107ms
Filling in 300000 into the JDK ConcurrentHashMap took 152ms
Filling in 300000 into the Eclipse map took 107ms
Filling in 300000 into the Trove map took 855ms
Filling in 300000 into the HPPC map took 93ms
Filling in 300000 into the FastUtil map took 163ms
1000 lookups average in JDK HashMap took: 550ns
1000 lookups average in JDK Concurrent HashMap took: 748ns
1000 lookups average in Eclipse Map took: 894ns
1000 lookups average in Trove Map took: 1033ns
1000 lookups average in HPPC Map took: 523ns
1000 lookups average in FastUtil Map took: 680ns
JDK HashMap:            43'809'895B
JDK Concurrent HashMap: 43'653'740B => save  0.36%
Eclipse Map:            35'755'084B => save 18.39%
Trove Map:              32'147'798B => save 26.62%
HPPC Map:               27'366'533B => save 37.53%
FastUtil Map:           31'560'889B => save 27.96%

すべての詳細とテストアプリケーションについては、my blog entry をご覧ください。

1
hotzst