web-dev-qa-db-fra.com

Java Optimisation des performances de HashMap / alternative

Je souhaite créer une grande carte de hachage, mais les performances de la fonction put() ne sont pas suffisantes. Des idées?

D'autres suggestions de structure de données sont les bienvenues, mais j'ai besoin de la fonctionnalité de recherche d'un Java Map:

map.get(key)

Dans mon cas, je veux créer une carte avec 26 millions d'entrées. En utilisant le standard Java HashMap, le taux de vente devient trop lent après 2 à 3 millions d'insertions.

En outre, est-ce que quelqu'un sait si l'utilisation de distributions de code de hachage différentes pour les clés peut aider?

Ma méthode de hashcode:

byte[] a = new byte[2];
byte[] b = new byte[3];
...

public int hashCode() {
    int hash = 503;
    hash = hash * 5381 + (a[0] + a[1]);
    hash = hash * 5381 + (b[0] + b[1] + b[2]);
    return hash;
}

J'utilise la propriété associative d'addition pour faire en sorte que des objets identiques aient le même hashcode. Les tableaux sont des octets dont les valeurs sont comprises entre 0 et 51. Les valeurs ne sont utilisées qu'une seule fois dans les tableaux. Les objets sont égaux si les tableaux a contiennent les mêmes valeurs (dans l'un ou l'autre ordre) et il en va de même pour le tableau b. Donc a = {0,1} b = {45,12,33} et a = {1,0} b = {33,45,12} sont égaux.

EDIT, quelques notes:

  • Quelques personnes ont critiqué l'utilisation d'une carte de hachage ou d'une autre structure de données pour stocker 26 millions d'entrées. Je ne vois pas pourquoi cela semblerait étrange. Cela ressemble à un problème classique de structures de données et d'algorithmes. J'ai 26 millions d'éléments et je veux pouvoir les insérer rapidement et les rechercher à partir d'une structure de données: donnez-moi la structure de données et les algorithmes.

  • Définition de la capacité initiale de la valeur par défaut Java HashMap) à 26 millions diminue la performance.

  • Certaines personnes ont suggéré d'utiliser des bases de données, mais dans d'autres situations, c'est l'option la plus judicieuse. Mais je pose en réalité une question sur les structures de données et les algorithmes: une base de données complète serait beaucoup plus lente et beaucoup plus lente qu’une bonne solution de datastructure (après tout, la base de données n’est qu'un logiciel mais aurait une communication et peut-être une surcharge de disque).

99
nash

Comme beaucoup de personnes l'ont souligné, la méthode hashCode() était à blâmer. Il ne générait qu'environ 20 000 codes pour 26 millions d'objets distincts. Soit une moyenne de 1 300 objets par seau de hachage = très très mauvais. Cependant, si je transforme les deux tableaux en un nombre en base 52, je suis assuré d'obtenir un code de hachage unique pour chaque objet:

public int hashCode() {       
    // assume that both a and b are sorted       
    return a[0] + powerOf52(a[1], 1) + powerOf52(b[0], 2) + powerOf52(b[1], 3) + powerOf52(b[2], 4);
}

public static int powerOf52(byte b, int power) {
    int result = b;
    for (int i = 0; i < power; i++) {
        result *= 52;
    }
    return result;
}

Les tableaux sont triés pour garantir que cette méthode respecte le contrat hashCode(), selon lequel les objets identiques ont le même code de hachage. En utilisant l'ancienne méthode, le nombre moyen d'options par seconde sur des blocs de 100 000, de 100 000 à 2 000 000, était de:

168350.17
109409.195
81344.91
64319.023
53780.79
45931.258
39680.29
34972.676
31354.514
28343.062
25562.371
23850.695
22299.22
20998.006
19797.799
18702.951
17702.434
16832.182
16084.52
15353.083

L'utilisation de la nouvelle méthode donne:

337837.84
337268.12
337078.66
336983.97
313873.2
317460.3
317748.5
320000.0
309704.06
310752.03
312944.5
265780.75
275540.5
264350.44
273522.97
270910.94
279008.7
276285.5
283455.16
289603.25

Beaucoup mieux. L'ancienne méthode s'est très vite estompée tandis que la nouvelle conserve un bon débit.

55
nash

Une chose que je remarque dans votre méthode hashCode() est que l'ordre des éléments dans les tableaux a[] Et b[] N'a pas d'importance. Ainsi, (a[]={1,2,3}, b[]={99,100}) Aura la même valeur que (a[]={3,1,2}, b[]={100,99}). En fait, toutes les touches k1 Et k2sum(k1.a)==sum(k2.a) et sum(k1.b)=sum(k2.b) entraîneront des collisions. Je suggère d'attribuer un poids à chaque position du tableau:

hash = hash * 5381 + (c0*a[0] + c1*a[1]);
hash = hash * 5381 + (c0*b[0] + c1*b[1] + c3*b[2]);

où, c0, c1 et c3 sont distinctes constantes (vous pouvez utiliser différentes constantes pour b si nécessaire). Cela devrait égaliser un peu plus les choses.

17
MAK

Pour élaborer sur Pascal: Comprenez-vous comment fonctionne HashMap? Vous avez un certain nombre d'emplacements dans votre table de hachage. La valeur de hachage de chaque clé est trouvée, puis mappée à une entrée de la table. Si deux valeurs de hachage correspondent à la même entrée - une "collision de hachage" -, HashMap construit une liste liée.

Les collisions de hachage peuvent tuer les performances d'une carte de hachage. Dans le cas extrême, si toutes vos clés ont le même code de hachage, ou si elles ont des codes de hachage différents mais qu'elles correspondent toutes au même emplacement, votre carte de hachage se transforme en une liste chaînée.

Donc, si vous rencontrez des problèmes de performances, la première chose à vérifier est la suivante: est-ce que je reçois une distribution aléatoire des codes de hachage? Sinon, vous avez besoin d'une meilleure fonction de hachage. Dans ce cas, "mieux" peut signifier "meilleur pour mon ensemble de données particulier". Par exemple, supposons que vous travailliez avec des chaînes et que vous preniez la longueur de la chaîne comme valeur de hachage. (Ce n'est pas comment String.hashCode de Java fonctionne, mais je ne fais que donner un exemple simple.) Si vos chaînes ont des longueurs très variables, allant de 1 à 10 000, et sont réparties de manière assez homogène sur cette plage, cela pourrait être un très bon fonction de hachage. Mais si vos chaînes sont toutes composées de 1 ou 2 caractères, ce serait une très mauvaise fonction de hachage.

Edit: Je devrais ajouter: chaque fois que vous ajoutez une nouvelle entrée, HashMap vérifie s'il s'agit d'un doublon. Lorsqu'il y a une collision de hachage, il doit comparer la clé entrante à chaque clé mappée à cet emplacement. Donc, dans le pire des cas où tout se gâche à un seul emplacement, la deuxième clé est comparée à la première clé, la troisième clé est comparée à # 1 et # 2, la quatrième clé est comparée à # 1, # 2 et # 3 , etc. Au moment où vous arrivez à la clé # 1 million, vous avez fait plus d’un billion de dollars.

@ Oscar: Hum, je ne vois pas en quoi c'est un "pas vraiment". C'est plus comme un "laissez-moi clarifier". Mais oui, il est vrai que si vous faites une nouvelle entrée avec la même clé qu'une entrée existante, cela écrasera la première entrée. C’est ce que je voulais dire lorsque j’ai parlé de rechercher des doublons dans le dernier paragraphe: chaque fois qu’une clé se divise dans le même emplacement, HashMap doit vérifier s’il s’agit d’un doublon d’une clé existante, ou s’ils se trouvent dans le même emplacement par simple fonction de hachage. Je ne sais pas si c'est le "but" d'un HashMap: je dirais que le "tout" est de pouvoir récupérer rapidement des éléments par clé.

Quoi qu'il en soit, cela n'affecte pas le "point entier" que j'essayais de dire: lorsque vous avez deux clés - oui, des clés différentes, pas la même clé qui apparaît à nouveau - cette carte est placée au même emplacement de la table. , HashMap construit une liste chaînée. Ensuite, comme il doit vérifier chaque nouvelle clé pour voir s’il s’agit bien du duplicata d’une clé existante, chaque tentative d’ajout d’une nouvelle entrée mappée à ce même emplacement doit poursuivre la liste liée en examinant chaque entrée existante pour voir s’il en est ainsi. est une copie d'une clé déjà vue, ou s'il s'agit d'une nouvelle clé.

Mise à jour longtemps après la publication d'origine

Je viens de recevoir un vote positif sur cette réponse six ans après la publication de mon message, ce qui m'a amené à relire la question.

La fonction de hachage donnée dans la question n'est pas une bonne hachage pour 26 millions d'entrées.

Il additionne a [0] + a [1] et b [0] + b [1] + b [2]. Il dit que les valeurs de chaque octet vont de 0 à 51, ce qui donne seulement (51 * 2 + 1) * (51 * 3 + 1) = 15 862 valeurs de hachage possibles. Avec 26 millions d'entrées, cela signifie une moyenne d'environ 1639 entrées par valeur de hachage. C'est beaucoup, beaucoup de collisions, nécessitant beaucoup de recherches séquentielles à travers des listes chaînées.

Le PO indique que différents ordres dans les tableaux a et b doivent être considérés comme égaux, c'est-à-dire [[1,2], [3,4,5]]. Égaux ([[2,1], [5,3,4] ]), et donc pour remplir le contrat, ils doivent avoir les mêmes codes de hachage. D'accord. Pourtant, il y a beaucoup plus de 15 000 valeurs possibles. Sa deuxième fonction de hachage proposée est bien meilleure, donnant une plage plus large.

Bien que quelqu'un d'autre ait commenté, il semble inapproprié pour une fonction de hachage de modifier d'autres données. Il serait plus logique de "normaliser" l’objet lorsqu’il est créé ou de laisser la fonction de hachage fonctionner à partir de copies des tableaux. De plus, utiliser une boucle pour calculer les constantes à chaque fois à travers la fonction est inefficace. Comme il n'y a que quatre valeurs ici, j'aurais soit écrit

return a[0]+a[1]*52+b[0]*52*52+b[1]*52*52*52+b[2]*52*52*52*52;

ce qui obligerait le compilateur à effectuer le calcul une fois au moment de la compilation; ou avoir 4 constantes statiques définies dans la classe.

En outre, le premier brouillon d'une fonction de hachage comporte plusieurs calculs qui ne font rien pour ajouter à la plage de résultats. Notez qu'il commence par définir hash = 503 qu'il multiplie par 5381 avant même de prendre en compte les valeurs de la classe. Alors ... en effet, il ajoute 503 * 5381 à chaque valeur. Qu'est-ce que cela accomplit? L'ajout d'une constante à chaque valeur de hachage ne fait que brûler des cycles de l'unité centrale sans rien accomplir d'utile. Leçon ici: L'ajout de complexité à une fonction de hachage n'est pas l'objectif. L’objectif est d’obtenir un large éventail de valeurs différentes, et pas seulement d’ajouter de la complexité à la complexité.

16
Jay

Entrer dans la zone grise du "sujet", mais nécessaire pour éliminer la confusion selon Oscar Reyes, suggère que plus de collisions de hachage est une bonne chose, car cela réduit le nombre d'éléments dans la HashMap. Je peux mal comprendre ce que dit Oscar, mais je ne semble pas être le seul: kdgregory, delfuego, Nash0 et moi, nous semblons tous partager la même (mauvaise) compréhension.

Si je comprends ce qu'Oscar dit à propos de la même classe avec le même hashcode, il propose qu'une seule instance d'une classe avec un hashcode donné soit insérée dans la HashMap. Par exemple, si j'ai une instance de SomeClass avec un hashcode de 1 et une seconde instance de SomeClass avec un hashcode de 1, une seule instance de SomeClass est insérée.

L'exemple Java Pastebin à http://Pastebin.com/f20af40b9 semble indiquer que ce qui précède résume correctement ce que propose Oscar.

Indépendamment de toute compréhension ou de tout malentendu, différentes instances de la même classe sont insérées pas insérées une seule fois dans la carte de hachage si elles ont le même code de hachage - pas jusqu'à ce qu'il soit déterminé si les clés sont égales ou pas. Le contrat de code de hachage requiert que des objets identiques aient le même code de hachage; cependant, il n'est pas nécessaire que les objets inégaux aient des codes de hachage différents (bien que cela puisse être souhaitable pour d'autres raisons) [1].

L'exemple Pastebin.com/f20af40b9 (auquel Oscar fait référence au moins deux fois) suit, mais légèrement modifié pour utiliser des assertions JUnit plutôt que des lignes à imprimer. Cet exemple est utilisé pour soutenir la proposition voulant que les mêmes codes de hachage provoquent des collisions et que, lorsque les classes sont identiques, une seule entrée soit créée (par exemple, une seule chaîne dans ce cas spécifique):

@Test
public void shouldOverwriteWhenEqualAndHashcodeSame() {
    String s = new String("ese");
    String ese = new String("ese");
    // same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    // same class
    assertEquals(s.getClass(), ese.getClass());
    // AND equal
    assertTrue(s.equals(ese));

    Map map = new HashMap();
    map.put(s, 1);
    map.put(ese, 2);
    SomeClass some = new SomeClass();
    // still  same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    assertEquals(s.hashCode(), some.hashCode());

    map.put(some, 3);
    // what would we get?
    assertEquals(2, map.size());

    assertEquals(2, map.get("ese"));
    assertEquals(3, map.get(some));

    assertTrue(s.equals(ese) && s.equals("ese"));
}

class SomeClass {
    public int hashCode() {
        return 100727;
    }
}

Cependant, le hashcode n'est pas l'histoire complète. Ce que l’exemple de Pastebin néglige, c’est que s et ese sont égaux: c’est la chaîne "ese". Ainsi, insérer ou récupérer le contenu de la carte en utilisant s ou ese ou "ese" Comme clé est équivalent car s.equals(ese) && s.equals("ese").

Un deuxième test démontre qu'il est erroné de conclure que des codes de hachage identiques sur la même classe sont la raison pour laquelle la clé -> valeur s -> 1 Est remplacée par ese -> 2 Lorsque map.put(ese, 2) est appelée dans tester un. Dans le test deux, s et ese ont toujours le même hashcode (tel que vérifié par assertEquals(s.hashCode(), ese.hashCode());) ET ils appartiennent à la même classe. Cependant, s et ese sont MyString instances dans ce test, pas Java String instances - à la seule différence pertinent pour ce test étant égal à: String s equals String ese dans le test un ci-dessus, alors que MyStrings s does not equal MyString ese dans le test deux:

@Test
public void shouldInsertWhenNotEqualAndHashcodeSame() {
    MyString s = new MyString("ese");
    MyString ese = new MyString("ese");
    // same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    // same class
    assertEquals(s.getClass(), ese.getClass());
    // BUT not equal
    assertFalse(s.equals(ese));

    Map map = new HashMap();
    map.put(s, 1);
    map.put(ese, 2);
    SomeClass some = new SomeClass();
    // still  same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    assertEquals(s.hashCode(), some.hashCode());

    map.put(some, 3);
    // what would we get?
    assertEquals(3, map.size());

    assertEquals(1, map.get(s));
    assertEquals(2, map.get(ese));
    assertEquals(3, map.get(some));
}

/**
 * NOTE: equals is not overridden so the default implementation is used
 * which means objects are only equal if they're the same instance, whereas
 * the actual Java String class compares the value of its contents.
 */
class MyString {
    String i;

    MyString(String i) {
        this.i = i;
    }

    @Override
    public int hashCode() {
        return 100727;
    }
}

Basé sur un commentaire plus tard, Oscar semble inverser ce qu'il a dit plus tôt et reconnaît l'importance des égaux. Cependant, il semble toujours que la notion d'égalité soit ce qui compte, et non la "même classe", n'est pas claire (souligné par moi):

"Pas vraiment. La liste est créée uniquement si le hachage est identique, mais la clé est différente. Par exemple, si String donne le hashcode 2345 et et Integer donne le même hashcode 2345, l'entier est inséré dans le list car String.equals (Integer) est false. Mais si vous avez la même classe (ou au moins .equals renvoie true) , la même entrée est alors Par exemple, new String ("un") et `new String (" un ") utilisés comme clés utiliseront la même entrée. En fait, il s’agit du point ENTIÈRE de HashMap en premier lieu! Voir pour vous-même: Pastebin.com/ f20af40b9 - Oscar Reyes "

par rapport aux commentaires précédents qui traitaient explicitement de l’importance d’une classe identique et d’un même hashcode, sans mention d’égal à égal:

"@ delfuego: Voyez vous-même: Pastebin.com/f20af40b9 Donc, dans cette question, la même classe est utilisée (attendez une minute, la même classe est utilisée non?). Ce qui implique que lorsque le même hachage est utilisé la même entrée est utilisée et il n'y a pas de "liste" d'entrées. - Oscar Reyes "

ou

"En fait, cela augmenterait les performances. Plus il y a de collisions, moins il y a d'entrées dans la table de hachage, moins de travail à faire. est sur la création d'objet où la performance se dégrade. - Oscar Reyes "

ou

"@ kdgregory: Oui, mais si la collision se produit avec différentes classes, pour la même classe (ce qui est le cas), la même entrée est utilisée. - Oscar Reyes"

Encore une fois, je peux mal comprendre ce qu'Oscar essayait de dire. Cependant, ses commentaires initiaux ont créé suffisamment de confusion pour qu’il soit prudent de tout clarifier à l’aide de tests explicites afin d’éviter les doutes.


[1] - De Effective Java, deuxième édition par Joshua Bloch:

  • Chaque fois qu'elle est appelée plusieurs fois sur le même objet lors de l'exécution d'une application, la méthode hashCode doit systématiquement renvoyer le même entier, à condition qu'aucune information utilisée dans les comparaisons égales sur l'objet ne soit modifiée. Cet entier n'a pas besoin de rester cohérent d'une exécution d'une application à une autre exécution de la même application.

  • Si deux objets sont égaux selon la méthode s (Obj ect) equal, l'appel de la méthode hashCode sur chacun des deux objets doit produire le même résultat entier.

  • Il n'est pas nécessaire que si deux objets ne soient pas égaux selon la méthode égal s(Object), l'appel de la méthode hashCode sur chacun des deux objets doit produire des résultats entiers distincts. Toutefois, le programmeur Il faut savoir que la production de résultats entiers distincts pour des objets inégaux peut améliorer les performances des tables de hachage.

7
Colin K

Je suggérerais une approche à trois volets:

  1. Exécuter Java avec plus de mémoire: Java -Xmx256M _ par exemple pour fonctionner avec 256 Mo. Utilisez plus si nécessaire et vous avez beaucoup de RAM.

  2. Cachez vos valeurs de hachage calculées comme suggéré par une autre affiche. Chaque objet ne calcule donc sa valeur de hachage qu'une seule fois.

  3. Utilisez un meilleur algorithme de hachage. Celui que vous avez publié renvoie le même hachage où a = {0, 1} que comme a = {1, 0}, toutes choses étant égales par ailleurs.

Utilisez ce que Java vous donne gratuitement.

public int hashCode() {
    return 31 * Arrays.hashCode(a) + Arrays.hashCode(b);
}

Je suis à peu près sûr que cela a beaucoup moins de chance de s’affronter que votre méthode hashCode existante, bien que cela dépende de la nature exacte de vos données.

7
Steve McLeod

Ma première idée est de vous assurer que vous initialisez votre HashMap correctement. Depuis le JavaDocs for HashMap :

Une instance de HashMap a deux paramètres qui affectent ses performances: la capacité initiale et le facteur de charge. La capacité correspond au nombre de compartiments dans la table de hachage et la capacité initiale est simplement la capacité au moment de la création de la table de hachage. Le facteur de charge est une mesure du taux de remplissage de la table de hachage avant que sa capacité ne soit automatiquement augmentée. Lorsque le nombre d'entrées dans la table de hachage dépasse le produit du facteur de charge et de la capacité actuelle, la table de hachage est réorganisée (c'est-à-dire que les structures de données internes sont reconstruites) afin que la table de hachage ait environ deux fois le nombre de compartiments.

Donc, si vous commencez avec une HashMap trop petite, alors chaque fois qu'il faut redimensionner tout les hachages sont recalculés ... ce qui pourrait soyez ce que vous ressentez lorsque vous atteignez le point d’insertion de 2 à 3 millions de dollars.

7
delfuego

Si les tableaux de votre hashCode publié sont des octets, vous aurez probablement beaucoup de doublons.

a [0] + a [1] sera toujours compris entre 0 et 512. l'ajout des b donnera toujours un nombre compris entre 0 et 768. multipliez-les et vous obtenez une limite supérieure de 400 000 combinaisons uniques, en supposant que vos données soient parfaitement distribuées. parmi toutes les valeurs possibles de chaque octet. Si vos données sont du tout régulières, vous aurez probablement beaucoup moins de sorties uniques de cette méthode.

5
Peter Recore

Si les touches ont un motif, vous pouvez diviser la carte en cartes plus petites et créer une carte index.

Exemple: Touches: 1,2,3, .... n 28 cartes de 1 million chacune. Carte de l'index: 1-1,000,000 -> Carte1 1,000,000-2,000,000 -> Carte2

Donc, vous allez faire deux recherches, mais le jeu de clés serait de 1 000 000 contre 28 000 000. Vous pouvez facilement le faire avec des modèles de piqûre aussi.

Si les clés sont complètement aléatoires, cela ne fonctionnera pas

4
coolest_head

Si les tableaux à deux octets que vous mentionnez sont votre clé entière, que les valeurs sont dans la plage 0-51, uniques et que l'ordre dans les tableaux a et b est insignifiant, mon calcul me dit qu'il n'y a que 26 millions de permutations possibles et que vous essayez probablement de remplir la carte avec des valeurs pour toutes les clés possibles.

Dans ce cas, le remplissage et la récupération des valeurs de votre magasin de données seraient bien sûr beaucoup plus rapides si vous utilisiez un tableau au lieu d'un HashMap et l'indexiez de 0 à 25989599.

4
jarnbjo

Je suis en retard ici, mais quelques commentaires à propos des grandes cartes:

  1. Comme discuté en détail dans d'autres publications, avec un bon hashCode (), 26 millions d'entrées dans une carte, ce n'est pas grave.
  2. Cependant, l’impact des cartes géantes sur le GC est un problème potentiellement caché.

Je suppose que ces cartes ont une longue durée de vie. c'est-à-dire que vous les remplissez et qu'ils restent pendant toute la durée de l'application. Je suppose également que l'application elle-même a une longue durée de vie, comme un serveur.

Chaque entrée d'un Java HashMap nécessite trois objets: la clé, la valeur et l'entrée qui les lie ensemble. Par conséquent, 26 millions d'entrées dans la carte correspondent à 26M * 3 == 78M objets. jusqu'à ce que vous atteigniez un GC complet. Vous avez ensuite un problème de pause dans le monde. Le GC examinera chacun des objets de 78M et déterminera qu'ils sont tous vivants. Les objets 78M + correspondent à beaucoup d'objets à regarder. Si votre application peut tolérer de longues pauses occasionnelles (peut-être plusieurs secondes), il n'y a aucun problème. Si vous essayez d'obtenir des garanties de latence, vous pourriez avoir un problème majeur (bien sûr, si vous voulez des garanties de latence, Java n'est pas la plate-forme à choisir. :)) Si les valeurs de vos cartes disparaissent rapidement, vous pouvez vous retrouver avec des collectes complètes fréquentes, ce qui aggrave considérablement le problème.

Je ne connais pas de solution satisfaisante à ce problème. Idées:

  • Il est parfois possible d'ajuster la taille des GC et des segments de mémoire de manière à empêcher "la plupart du temps" les GC complètes.
  • Si le contenu de votre carte manque beaucoup, vous pouvez essayer FastMap de Javolution - il peut regrouper les objets Entry, ce qui pourrait réduire la fréquence des collectes complètes.
  • Vous pouvez créer votre propre implément cartographique et gérer explicitement la mémoire sur byte [] (c'est-à-dire échanger un processeur pour obtenir une latence plus prévisible en sérialisant des millions d'objets dans un seul octet [] - euh!)
  • N'utilisez pas Java pour cette partie - parlez à une sorte de base de données en mémoire prévisible sur un socket
  • J'espère que le nouveau G1 collecteur aidera (s'applique principalement au cas de fort taux de désabonnement)

Quelques réflexions de quelqu'un qui a passé beaucoup de temps avec les cartes géantes en Java.


4
overthink

HashMap a une capacité initiale et les performances de HashMap dépendent très fortement de hashCode qui produit les objets sous-jacents.

Essayez de modifier les deux.

4
Mykola Golubyev

Dans mon cas, je veux créer une carte avec 26 millions d'entrées. En utilisant le standard Java HashMap, le taux de vente devient extrêmement lent après 2 à 3 millions d'insertions.

De mon expérience (projet étudiant en 2009):

  • J'ai construit un arbre noir rouge pour 100 000 nœuds de 1 à 100 000. Cela a pris 785,68 secondes (13 minutes). Et je n'ai pas réussi à construire RBTree pour 1 million de nœuds (comme vos résultats avec HashMap).
  • Utilisation de "Prime Tree", la structure de données de mon algorithme. Je pourrais construire un arbre/une carte pour 10 millions de nœuds en 21,29 secondes (RAM: 1,97 Go). Le coût de la valeur de la recherche est égal à O (1).

Remarque: "Prime Tree" fonctionne mieux avec des "touches continues" de 1 à 10 millions. Pour travailler avec des clés comme HashMap, nous avons besoin de quelques ajustements mineurs.


Alors, qu'est-ce que #PrimeTree? En bref, il s’agit d’une structure de données arborescente semblable à Binary Tree, avec des branches dont les nombres sont des nombres premiers (au lieu de "2" -binary).

2
Hoàng Đặng

Vous pouvez essayer d'utiliser une base de données en mémoire telle que HSQLDB .

2
Adrian

SQLite vous permet de l'utiliser en mémoire.

1
JRL

Vous devez d’abord vérifier que vous utilisez correctement Map, bonne méthode hashCode () pour les clés, capacité initiale de Map, bonne implémentation de la carte, etc., comme décrit par de nombreuses autres réponses.

Ensuite, je suggérerais d'utiliser un profileur pour voir ce qui se passe réellement et où le temps d'exécution est passé. Par exemple, la méthode hashCode () est-elle exécutée des milliards de fois?

Si cela ne vous aide pas, pourquoi ne pas utiliser quelque chose comme EHCache ou memcached ? Oui, ce sont des produits pour la mise en cache, mais vous pouvez les configurer de manière à ce qu'ils aient une capacité suffisante et ne suppriment jamais aucune valeur du stockage en cache.

Une autre option serait un moteur de base de données plus léger que le SGBDR SQL complet. Quelque chose comme Berkeley DB , peut-être.

Notez que je n'ai personnellement aucune expérience des performances de ces produits, mais ils pourraient en valoir la peine.

1
Juha Syrjälä

Vous pouvez essayer de mettre en cache le code de hachage calculé sur l’objet clé.

Quelque chose comme ça:

public int hashCode() {
  if(this.hashCode == null) {
     this.hashCode = computeHashCode();
  }
  return this.hashCode;
}

private int computeHashCode() {
   int hash = 503;
   hash = hash * 5381 + (a[0] + a[1]);
   hash = hash * 5381 + (b[0] + b[1] + b[2]);
   return hash;
}

Bien sûr, vous devez faire attention à ne pas changer le contenu de la clé après que le hashCode ait été calculé pour la première fois.

Éditer: Il semble que la mise en cache avec des valeurs de code ne vaut pas la peine d’ajouter chaque clé à une carte. Dans d'autres situations, cela pourrait être utile.

1
Juha Syrjälä

Une autre affiche a déjà indiqué que la mise en œuvre de votre hashcode entraînerait de nombreuses collisions en raison de la façon dont vous ajoutez des valeurs. Si vous examinez l'objet HashMap dans un débogueur, vous constaterez que vous avez peut-être 200 valeurs de hachage distinctes, avec des chaînes de compartiment extrêmement longues.

Si vous avez toujours des valeurs comprises entre 0 et 51, chacune de ces valeurs prendra 6 bits à représenter. Si vous avez toujours 5 valeurs, vous pouvez créer un hashcode 30 bits avec des décalages à gauche et des ajouts:

    int code = a[0];
    code = (code << 6) + a[1];
    code = (code << 6) + b[0];
    code = (code << 6) + b[1];
    code = (code << 6) + b[2];
    return code;

Le décalage à gauche est rapide, mais vous laissera des codes de hachage qui ne sont pas distribués uniformément (car 6 bits impliquent une plage de 0..63). Une alternative consiste à multiplier le hachage par 51 et à ajouter chaque valeur. Cela ne sera toujours pas parfaitement distribué (par exemple, {2,0} et {1,52} entreront en collision) et sera plus lent que le décalage.

    int code = a[0];
    code *= 51 + a[1];
    code *= 51 + b[0];
    code *= 51 + b[1];
    code *= 51 + b[2];
    return code;
1
kdgregory

In Effective Java: Guide du langage de programmation (série Java)

Au chapitre 3, vous trouverez de bonnes règles à suivre lors du calcul de hashCode ().

Spécialement:

Si le champ est un tableau, traitez-le comme si chaque élément était un champ séparé. En d’autres termes, calculez un code de hachage pour chaque élément significatif en appliquant ces règles de manière récursive, puis combinez ces valeurs à l’étape 2.b. Si chaque élément d'un champ de tableau est significatif, vous pouvez utiliser l'une des méthodes Arrays.hashCode ajoutées à la version 1.5.

1
amanas

Comme indiqué, votre implémentation de hashcode a trop de collisions, et sa résolution devrait permettre d'obtenir des performances décentes. De plus, la mise en cache de hashCodes et l'implémentation efficace d'égal à égal aideront.

Si vous avez besoin d'optimiser encore plus:

Selon votre description, il n'y a que (52 * 51/2) * (52 * 51 * 50/6) = 29304600 clés différentes (dont 26000000, c'est-à-dire environ 90%, seront présentes). Par conséquent, vous pouvez concevoir une fonction de hachage sans collision et utiliser un tableau simple plutôt qu'une table de hachage pour stocker vos données, réduisant ainsi la consommation de mémoire et augmentant la vitesse de recherche:

T[] array = new T[Key.maxHashCode];

void put(Key k, T value) {
    array[k.hashCode()] = value;

T get(Key k) {
    return array[k.hashCode()];
}

(En règle générale, il est impossible de concevoir une fonction de hachage efficace, sans collision, qui classe bien, ce qui explique pourquoi HashMap tolérera les collisions, ce qui entraînera des frais généraux.)

En supposant que a et b soient triés, vous pouvez utiliser la fonction de hachage suivante:

public int hashCode() {
    assert a[0] < a[1]; 
    int ahash = a[1] * a[1] / 2 
              + a[0];

    assert b[0] < b[1] && b[1] < b[2];

    int bhash = b[2] * b[2] * b[2] / 6
              + b[1] * b[1] / 2
              + b[0];
    return bhash * 52 * 52 / 2 + ahash;
}

static final int maxHashCode = 52 * 52 / 2 * 52 * 52 * 52 / 6;  

Je pense que c'est sans collision. Prouver cela est laissé comme un exercice pour le lecteur incliné mathématiquement.

1
meriton

Avez-vous envisagé d'utiliser une base de données intégrée pour le faire? Regardez Berkeley DB . Il est open-source, appartenant maintenant à Oracle.

Il stocke tout en tant que paire clé-> valeur, ce n'est pas un SGBDR. et il vise à être rapide.

1
coolest_head

Attribuez une grande carte au début. Si vous savez qu'il aura 26 millions d'entrées et que vous avez la mémoire pour cela, faites une new HashMap(30000000).

Êtes-vous sûr que vous avez assez de mémoire pour 26 millions d'entrées avec 26 millions de clés et de valeurs? Cela me rappelle beaucoup de mémoire. Etes-vous sûr que la collecte des ordures se porte toujours bien entre 2 et 3 millions de marks? Je pourrais imaginer cela comme un goulot d'étranglement.

0
ReneS

Les méthodes de hachage populaires utilisées ne sont pas vraiment très bonnes pour les grands ensembles et, comme indiqué ci-dessus, le hachage utilisé est particulièrement mauvais. Mieux vaut utiliser un algorithme de hachage avec un mélange élevé et une couverture telle que BuzHash (exemple de mise en œuvre à http://www.Java2s.com/Code/Java/Development-Class/AveryefficientjavahashalgorithmbasedontheBuzHashalgoritm.htm )

0
Paul

Vous pouvez essayer deux choses:

  • Faites en sorte que votre méthode hashCode renvoie quelque chose de plus simple et plus efficace, tel qu'un int

  • Initialisez votre carte en tant que:

    Map map = new HashMap( 30000000, .95f );
    

Ces deux actions réduiront énormément le nombre de modifications apportées par la structure et sont assez faciles à tester, je pense.

Si cela ne fonctionne pas, envisagez d'utiliser un stockage différent, tel qu'un SGBDR.

[~ # ~] éditer [~ # ~]

Il est étrange que le réglage de la capacité initiale réduise les performances dans votre cas.

Voir à partir du javadocs :

Si la capacité initiale est supérieure au nombre maximal d'entrées divisé par le facteur de charge, aucune opération de remise en place ne se produira.

J'ai fait un microbeachmark (qui n'est absolument pas définitif mais prouve au moins ce point)

$cat Huge*Java
import Java.util.*;
public class Huge {
    public static void main( String [] args ) {
        Map map = new HashMap( 30000000 , 0.95f );
        for( int i = 0 ; i < 26000000 ; i ++ ) { 
            map.put( i, i );
        }
    }
}
import Java.util.*;
public class Huge2 {
    public static void main( String [] args ) {
        Map map = new HashMap();
        for( int i = 0 ; i < 26000000 ; i ++ ) { 
            map.put( i, i );
        }
    }
}
$time Java -Xms2g -Xmx2g Huge

real    0m16.207s
user    0m14.761s
sys 0m1.377s
$time Java -Xms2g -Xmx2g Huge2

real    0m21.781s
user    0m20.045s
sys 0m1.656s
$

Ainsi, l'utilisation de la capacité initiale passe de 21 à 16 en raison de la remise en phase. Cela nous laisse avec votre méthode hashCode comme "domaine d'opportunité";)

[~ # ~] éditer [~ # ~]

N'est-ce pas le HashMap

Selon votre dernière édition.

Je pense que vous devriez vraiment profiler votre application et voir où est utilisée la mémoire/le cpu.

J'ai créé une classe implémentant votre même hashCode

Ce code de hachage donne des millions de collisions, puis les entrées dans HashMap sont réduites de façon spectaculaire.

Je passe de 21 ans, 16 ans lors de mon précédent test à 10 ans et 8 ans. La raison en est que le hashCode provoque un grand nombre de collisions et que vous ne stockez pas les 26M objets que vous pensez, mais un nombre beaucoup plus bas (environ 20k, je dirais). Ainsi:

Les problèmes N'EST PAS LA HASHMAP se trouvent ailleurs dans votre code.

Il est temps d’avoir un profileur et de savoir où. Je pense que c'est sur la création de l'élément ou probablement que vous écrivez sur un disque ou recevez des données du réseau.

Voici ma mise en œuvre de votre classe.

note Je n'ai pas utilisé une plage de 0 à 51 comme vous, mais -126 à 127 pour mes valeurs et admets répété, c'est parce que j'ai fait ce test. avant de mettre à jour votre question

La seule différence est que votre classe aura plus de collisions, donc moins d'éléments stockés dans la carte.

import Java.util.*;
public class Item {

    private static byte w = Byte.MIN_VALUE;
    private static byte x = Byte.MIN_VALUE;
    private static byte y = Byte.MIN_VALUE;
    private static byte z = Byte.MIN_VALUE;

    // Just to avoid typing :) 
    private static final byte M = Byte.MAX_VALUE;
    private static final byte m = Byte.MIN_VALUE;


    private byte [] a = new byte[2];
    private byte [] b = new byte[3];

    public Item () {
        // make a different value for the bytes
        increment();
        a[0] = z;        a[1] = y;    
        b[0] = x;        b[1] = w;   b[2] = z;
    }

    private static void increment() {
        z++;
        if( z == M ) {
            z = m;
            y++;
        }
        if( y == M ) {
            y = m;
            x++;
        }
        if( x == M ) {
            x = m;
            w++;
        }
    }
    public String toString() {
        return "" + this.hashCode();
    }



    public int hashCode() {
        int hash = 503;
        hash = hash * 5381 + (a[0] + a[1]);
        hash = hash * 5381 + (b[0] + b[1] + b[2]);
        return hash;
    }
    // I don't realy care about this right now. 
    public boolean equals( Object other ) {
        return this.hashCode() == other.hashCode();
    }

    // print how many collisions do we have in 26M items.
    public static void main( String [] args ) {
        Set set = new HashSet();
        int collisions = 0;
        for ( int i = 0 ; i < 26000000 ; i++ ) {
            if( ! set.add( new Item() ) ) {
                collisions++;
            }
        }
        System.out.println( collisions );
    }
}

Utiliser cette classe a Key pour le programme précédent

 map.put( new Item() , i );

donne moi:

real     0m11.188s
user     0m10.784s
sys 0m0.261s


real     0m9.348s
user     0m9.071s
sys  0m0.161s
0
OscarRyz

J'ai fait un petit test il y a quelque temps avec une liste vs une hashmap, chose amusante de parcourir la liste et de trouver l'objet prend le même temps, en millisecondes, que d'utiliser la fonction hashmaps get ... juste un fyi. Oh oui, la mémoire est un gros problème lorsque l'on travaille avec hashmaps de cette taille.

0
Gerrit Brink

Peut-être essayez d'utiliser si vous avez besoin d'être synchronisé

http://commons.Apache.org/collections/api/org/Apache/commons/collections/FastHashMap.html

0
IAdapter