web-dev-qa-db-fra.com

Alternative de carte pour les valeurs primitives

J'ai effectué un profilage sur mon application et l'un des résultats a révélé qu'environ 18% de la mémoire du tas est utilisée par des objets de type Double. Il s'avère que ces objets sont les valeurs dans Maps, où je ne peux pas utiliser le type primitif.

Mon raisonnement est que le type primitif double consomme moins de mémoire que son objet Double. Existe-t-il un moyen d’avoir une carte comme une structure de données, qui accepterait n’importe quel type comme clé et une primitive double comme valeur?

Les opérations principales seraient:

  • insertion (probablement une seule fois)
  • Lookup (contient par clé)
  • Récupération (par clé)
  • Itération

Les cartes typiques que j'ai sont:

  • HashMap<T, HashMap<NodeData<T>, Double>> graph
  • HashMap<Point2D, Boolean> onSea (mais pas une valeur double)
  • ConcurrentHashMap<Point2D, HashMap<Point2D, Double>>

Tous utilisés avec Java 8.

Addenda

Je ne m'intéresse surtout pas aux cadres qui offrent une solution à ce type de carte, mais à ce qui doit être pris en compte pour résoudre ces problèmes. Si vous le souhaitez, quels sont les concepts/idées/approches qui sous-tendent un tel cadre. Ou bien la solution peut aussi se situer à un autre niveau, où les cartes sont remplacées par des objets suivant un certain motif, comme l'a souligné @Ilmari Karonen dans sa réponse.

22
hotzst

D'autres ont déjà suggéré plusieurs implémentations tierces de cartes à valeurs primitives. Pour être complet, je voudrais mentionner quelques moyens pour vous débarrasser des cartes entièrement que vous voudrez peut-être envisager. Ces solutions ne seront pas toujours possibles, mais lorsqu'elles le seront, elles seront généralement plus rapides et plus efficaces en mémoire que n'importe quelle carte.

Alternative 1: Utilisez de vieux tableaux simples.

Un simple tableau double[] n'est peut-être pas aussi sexy qu'une carte sophistiquée, mais très peu peut le battre en termes de compacité et de rapidité d'accès.

Bien sûr, les tableaux ont de nombreuses limitations: leur taille est fixe (bien que vous puissiez toujours créer un nouveau tableau et y copier l'ancien contenu) et leurs clés ne peuvent être que de petits entiers positifs qui, par souci d'efficacité, doivent être raisonnablement corrects. dense (c’est-à-dire que le nombre total de clés utilisées doit être une fraction raisonnablement grande de la valeur de clé la plus élevée). Mais si cela se produit pour vos clés ou si vous pouvez le faire, les tableaux de valeurs primitives peuvent être très efficaces.

En particulier, si vous pouvez affecter un ID unique de petit entier unique à chaque objet clé, vous pouvez utiliser cet ID en tant qu'index dans un tableau. De même, si vous stockez déjà vos objets dans un tableau (par exemple dans le cadre d’une structure de données plus complexe) et que vous les recherchez par index, vous pouvez simplement utiliser le même index pour rechercher des valeurs de métadonnées supplémentaires dans un autre tableau.

Vous pouvez même vous dispenser de l'exigence d'unicité d'identifiant si vous implémentez un mécanisme de traitement des collisions, mais à ce stade, vous êtes sur la bonne voie pour implémenter votre propre table de hachage. Dans certains cas, cela signifie que pourrait avoir un sens, mais généralement à ce stade, il est probablement plus facile d'utiliser une implémentation tierce existante.

Alternative 2: Personnalisez vos objets.

Au lieu de conserver une carte des objets clés en valeurs primitives, pourquoi ne pas simplement transformer ces valeurs en propriétés des objets eux-mêmes? C’est, après tout, ce qu’est la programmation orientée objet: regrouper des données connexes dans des objets significatifs.

Par exemple, au lieu de conserver un HashMap<Point2D, Boolean> onSea, pourquoi ne pas simplement attribuer à vos points une propriété booléenne onSea? Bien sûr, vous aurez besoin de définir votre propre classe de points personnalisée pour cela, mais rien ne vous empêche de le faire pour étendre la classe standard Point2D si vous le souhaitez, afin de pouvoir passer vos points personnalisés à toute méthode qui attend un Point2D.

Encore une fois, cette approche peut ne pas toujours fonctionner directement, par ex. si vous devez travailler avec des classes que vous ne pouvez pas modifier (voir ci-dessous) ou si les valeurs que vous souhaitez stocker sont associées à plusieurs objets (comme dans votre ConcurrentHashMap<Point2D, HashMap<Point2D, Double>>).

Toutefois, dans ce dernier cas, vous pourrez toujours résoudre le problème en modifiant de nouveau la représentation de vos données. Par exemple, au lieu de représenter un graphique pondéré sous la forme d'un Map<Node, Map<Node, Double>>, vous pouvez définir une classe Edge telle que:

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

puis ajoutez une propriété Edge[] (ou Vector<Edge>) à chaque nœud contenant des arêtes connectées à ce nœud.

Alternative 3: Combinez plusieurs cartes en une.

Si vous avez plusieurs cartes avec les mêmes clés et que vous ne pouvez pas simplement transformer les valeurs en nouvelles propriétés des objets de clé comme suggéré ci-dessus, envisagez de les regrouper dans une seule classe de métadonnées et de créer une seule carte à partir des clés en objets de cette classe. Par exemple, au lieu d'un Map<Item, Double> accessFrequency et d'un Map<Item, Long> creationTime, envisagez de définir une classe de métadonnées unique comme:

class ItemMetadata {
    double accessFrequency;
    long creationTime;
}

et ayant un seul Map<Item, ItemMetadata> pour stocker toutes les valeurs de métadonnées. Cela économise davantage en mémoire que plusieurs cartes et peut également vous faire gagner du temps en évitant les recherches de cartes redondantes.

Dans certains cas, par souci de commodité, vous pouvez également inclure dans chaque objet de métadonnées une référence à son objet principal correspondant, afin de pouvoir accéder à la fois par le biais d'une référence unique à l'objet de métadonnées. Qui se jette naturellement dans ...

Alternative 4: Utilisez un décorateur.

Si vous ne pouvez pas ajouter directement des propriétés de métadonnées supplémentaires dans les objets de clé, combinez les deux alternatives précédentes, envisagez plutôt de les envelopper avec décorateurs pouvant contenir les valeurs supplémentaires. Ainsi, par exemple, au lieu de créer directement votre propre classe de points avec des propriétés supplémentaires, vous pouvez simplement faire quelque chose comme:

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

Si vous le souhaitez, vous pouvez même transformer cette enveloppe en décorateur à part entière en implémentant le transfert de méthode, mais même une simple enveloppe "muette" peut suffire à de nombreuses fins.

Cette approche est particulièrement utile si vous pouvez ensuite stocker et utiliser uniquement les wrappers, de sorte que vous n’ayez jamais besoin de rechercher le wrapper correspondant à un objet non encapsulé. Bien sûr, si vous avez besoin de le faire de temps en temps (par exemple parce que vous ne recevez que les objets non enveloppés d'un autre code), vous pouvez le faire avec un seul Map<Point2D, PointWrapper>, mais vous revenez à la fin. alternative précédente.

10
Ilmari Karonen

Collections Eclipse a objet et cartes primitives et a des versions Mutable et Immutable pour les deux. 

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);

Une MutableMap peut être utilisée comme une fabrique pour une ImmutableMap dans Eclipse Collections en appelant toImmutable comme je l'ai fait dans l'exemple ci-dessus. Les mappes mutables et immuables partagent une interface parent commune, qui dans le cas de MutableObjectDoubleMap et ImmutableObjectDoubleMap ci-dessus, s'appelle ObjectDoubleMap.

Eclipse Collections possède également des versions synchronisées et non modifiables pour tous les conteneurs mutables de la bibliothèque. Le code suivant vous donnera une vue synchronisée enroulée autour des cartes primitives.

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);

Cette comparaison de performances de grandes cartes a été publiée il y a quelques années. 

Grand aperçu de HashMap: JDK, FastUtil, Goldman Sachs, HPPC, Koloboke, Trove - version de janvier 2015

GS Collections a depuis été migré vers la fondation Eclipse et est maintenant Eclipse Collections.

Remarque: je suis un partisan des collections Eclipse.

15
Donald Raab

Ce que vous recherchez est un Object2DoubleOpenHashMap from fastutil (Collections Framework avec un faible encombrement mémoire et un accès et une insertion rapides) qui fournit des méthodes de type double getDouble(Object k) et double put(K k, double v) .

Par exemple:

// 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");

La classe Object2DoubleOpenHashMap est une implémentation réelle d'une Map qui n'est pas thread-safe, mais vous pouvez toujours utiliser la méthode de l'utilitaire Object2DoubleMaps.synchronize(Object2DoubleMap<K> m) pour la rendre thread-safe grâce à un décorateur.

Le code de création serait alors:

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

Pour avoir une meilleure idée de la façon dont ces différentes bibliothèques se comparent, j'ai établi un petit repère qui vérifie les performances de:

  • temps total pour 300'000 insertions
  • durée moyenne d'une vérification des confinements avec 1000 échantillons figurant sur la carte
  • Taille de la mémoire de la structure de données J'ai jeté un œil à la structure de type Map- qui prend un String en tant que clé et double en tant que valeur. Les cadres vérifiés sont Eclipse Collection , HPPC , Trove et FastUtil , ainsi que pour la comparaison HashMap et ConcurrentHashMap.

En bref, voici les résultats:

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%

Pour tous les détails, ainsi que l'application de test, jetez un coup d'œil à mon entrée de blog .

1
hotzst