102 votes

Optimisation des performances HashMap Java / alternative

Je veux créer une grande table de hachage, mais l' put() de la performance n'est pas assez bon. Des idées?

D'autres structures de données les suggestions sont les bienvenues, mais j'ai besoin de la fonctionnalité de recherche de Java Carte:

map.get(key)

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

Aussi, personne ne sait si à l'aide de différents code de hachage de distributions pour les clés pourrait aider?

Ma méthode 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;
}

Je suis en utilisant l'associativité de l'addition pour s'assurer que l'égalité des objets ont le même hashcode. Les tableaux sont des octets avec des valeurs dans la plage 0 - 51. Les valeurs sont utilisées seulement une fois dans la matrice. Les objets sont égaux si l'un des tableaux contiennent les mêmes valeurs (dans l'ordre) et il en va de même pour le b tableau. Donc a = {0,1} b = {45,12,33} et a = {1,0} b = {33,45,12} sont égaux.

EDIT, quelques remarques:

  • Peu de gens ont critiqué à l'aide d'un algorithme de hachage carte ou une autre structure de données pour stocker les 26 millions d'entrées. Je ne vois pas pourquoi ce serait étrange. Il ressemble à un classique de structures de données et algorithmes de problème pour moi. J'ai 26 millions d'articles et je veux être rapidement en mesure de les insérer dans et les rechercher à partir d'une structure de données: donnez-moi la structure de données et algorithmes.

  • Réglage de la capacité initiale de la valeur par défaut de Java HashMap à 26 millions d' diminue les performances.

  • Certaines personnes ont suggéré l'utilisation des bases de données, dans d'autres situations, c'est certainement le option. Mais je suis vraiment demander des structures de données et algorithmes question, une base de données complète serait exagéré, et beaucoup plus lent qu'un bon discbased solution (après tout, la base de données est un logiciel, mais qui aurait de la communication et, éventuellement, de surcharge de disque).

56voto

nash Points 1325

Comme de nombreuses personnes ont souligné l' hashCode() méthode était à blâmer. C'est seulement générer près de 20 000 codes pour 26 millions d'objets distincts. C'est une moyenne de 1 300 objets par compartiment de hachage = très très mauvais. Cependant, si je tourne les deux tableaux en un nombre en base 52 je suis assuré d'obtenir un unique code de hachage pour chaque objet:

public int hashCode() {       
    Arrays.sort(a);
    Arrays.sort(b);       
    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 afin d'assurer cette méthode répond à l' hashCode() contrat que l'égalité d'objets ont le même code de hachage. En utilisant l'ancienne méthode de la moyenne du nombre de place par seconde sur des blocs de 100 000 met 100 000 à 2 000 000 d'été:

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'aide 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, beaucoup mieux. L'ancienne méthode à queue très rapidement alors que la nouvelle garde un bon débit.

18voto

MAK Points 12571

Une chose que je constate 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}) hachage à la même valeur que a[]={3,1,2}, b[]={100,99}). En fait toutes les clés k1 et k2 où somme(k1.a)==sum(k2.a) et la somme(k1.b)=somme(k2.b) entraînera dans des collisions. Je suggère l'attribution d'un poids à chaque position de la matrice:

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 des constantes (vous pouvez utiliser différentes constantes pour b si nécessaire). Qui devrait même sortir des trucs un peu plus.

17voto

Jay Points 14781

Pour des précisions sur Pascal: avez-vous compris comment une HashMap œuvres? Vous avez un certain nombre d'emplacements dans votre table de hachage. La valeur de hachage pour chaque clé est trouvée, puis cartographiés à une entrée dans la table. Si deux valeurs de hachage de la carte à la même entrée-un "hash collision" -- HashMap construit une liste liée.

Les collisions de hachage peut tuer la performance d'un hachage de la carte. Dans le cas extrême, si tous vos clés ont le même code de hachage, ou si elles ont des codes de hachage, mais ils ont tous une carte à l'emplacement de même, alors votre hash map transforme en une liste liée.

Donc, si vous voyez des problèmes de performances, la première chose que je voudrais vérifier est: Suis-je un hasard-à la recherche de la distribution des codes de hachage? Si non, vous avez besoin d'une meilleure fonction de hachage. Eh bien, "mieux" dans ce cas, peut signifier "le mieux pour mon jeu de données particulier". Comme, supposons que vous travailliez avec des chaînes, et vous avez pris la longueur de la chaîne de la valeur de hachage. (Pas la façon dont Java de la Chaîne.hashCode fonctionne, mais je suis juste un simple exemple.) Si vos cordes sont très différentes longueurs, de 1 à 10 000, et sont assez uniformément répartis sur toute cette gamme, que ce pourrait être une très bonne fonction de hachage. Mais si vos cordes sont tous les 1 ou 2 caractères, ce serait une très mauvaise fonction de hachage.

Edit: je me dois d'ajouter: Chaque fois que vous ajoutez une nouvelle entrée, HashMap vérifie si c'est un doublon. Quand il y a un hash collision, il doit comparer les entrants clé contre toutes les clés qui mappé à cet emplacement. Donc dans le pire des cas où tout hachages pour un logement unique, la deuxième clé, c'est par rapport à la première, la troisième clé est comparé à #1 et #2, la quatrième clé est comparé à #1, #2, et #3, etc. Au moment où vous arrivez à la clé n ° 1 million de dollars, vous avez fait plus d'un billion de compare.

@Oscar: Umm, je ne vois pas en quoi c'est un "pas vraiment". C'est plus comme un "permettez-moi de clarifier". Mais oui, c'est vrai que si vous faites une nouvelle entrée avec la même clé comme une entrée existante, que cela remplace la première entrée. C'est ce que je voulais dire quand j'ai parlé de la recherche de doublons dans le dernier paragraphe: Chaque fois qu'une touche hachages pour le même logement, HashMap doit vérifier si c'est un doublon d'une clé existante, ou si ils sont juste dans le même logement, par hasard, de la fonction de hachage. Je ne sais pas si c'est le "point" d'un HashMap: je dirais que le "point" est que vous pouvez récupérer les éléments clés rapidement.

Mais de toute façon, cela n'affecte pas le "tout point" que j'essayais de faire: Quand vous avez deux clés -- oui, clés différentes, pas la même clé, en montrant à nouveau-que la carte à l'emplacement de même dans le tableau, HashMap construit une liste liée. Ensuite, parce qu'il doit vérifier chaque nouvelle clé pour voir si elle est en fait un double de la clé existante, chaque tentative pour ajouter une nouvelle entrée qui correspond à ce même logement doit chasser la liste liée de l'examen de chaque entrée existante pour voir si c'est un doublon d'un déjà-vu clé, ou si c'est une nouvelle clé.

7voto

Steve McLeod Points 19016

Je vous suggère une approche à trois volets:

  1. Exécuter Java avec plus de mémoire: java -Xmx256M par exemple pour exécuter avec 256 méga-octets. L'utilisation plus si nécessaire et vous avez beaucoup de RAM.

  2. Cache de votre calculées les valeurs de hachage comme l'a suggéré une autre affiche, de sorte que chaque objet ne calcule sa valeur de hachage d'une fois.

  3. Utiliser un meilleur algorithme de hachage. Celui que vous avez posté serait de retour le même hash, où a = {0, 1} comme il le ferait, où a ={1, 0}, toutes choses étant égales par ailleurs.

Utiliser ce que Java vous donne gratuitement.

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

Je suis sûr que cela a beaucoup moins de chance d'aller à l'encontre de votre méthode hashCode, même si cela dépend de la nature exacte de vos données.

7voto

Colin Kershaw Points 155

Entrer dans la zone grise de la "sur/hors sujet", mais nécessaires pour éviter toute confusion concernant Oscar Reyes suggestion que plus les collisions de hachage est une bonne chose, car cela réduit le nombre d'éléments dans la table de hachage. Je peut mal comprendre ce que Oscar est dire, mais je ne semble pas être le seul: kdgregory, delfuego, Nash0, et j'ai tous semblent partager le même (sig)de la compréhension.

Si je comprends ce que Oscar est dit à propos de la même classe avec le même hashcode, il propose qu'une seule instance d'une classe avec un hashcode sera inséré dans la table de hachage. Par exemple, si j'ai une instance de SomeClass avec un hashcode de 1 et une deuxième instance de SomeClass avec un hashcode de 1, une seule instance de SomeClass est inséré.

La Java pastebin exemple à http://pastebin.com/f20af40b9 semble indiquer ci-dessus correctement résume ce que Oscar propose.

Indépendamment de toute compréhension ou d'incompréhension, ce qui se passe est différentes instances de la même classe de ne pas obtenir inséré qu'une seule fois dans la table de hachage si elles ont le même hashcode - pas jusqu'à ce qu'elle a déterminé si les touches sont égaux ou non. Le hashcode contrat exige que l'égalité des objets ont le même hashcode; toutefois, il ne nécessite pas que l'inégalité des objets sont différents hashcodes (même si cela peut être souhaitable pour d'autres raisons)[1].

L'pastebin.com/f20af40b9 exemple (Oscar renvoie au moins à deux reprises), mais légèrement modifié pour utiliser JUnit affirmations plutôt que printlines. Cet exemple est utilisé à l'appui de la proposition que le même hashcodes provoquer des collisions et lorsque les classes sont les mêmes qu'une entrée est créée (par exemple, une seule Chaîne de caractères dans ce cas précis):

@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 le pastebin exemple néglige le fait que les deux s et ese sont égaux: ils sont à la fois la chaîne "ese". Ainsi, l'insertion ou de l'obtention du contenu de la carte à l'aide de s ou ese ou "ese" que les clés sont toutes équivalentes, car s.equals(ese) && s.equals("ese").

Un deuxième test démontre qu'il est erroné de conclure que les mêmes hashcodes sur la même classe est la raison pour laquelle la clé -> valeur s -> 1 est remplacé par ese -> 2 lorsque map.put(ese, 2) est appelé en tester un. Dans l'essai deux, s et ese ont toujours le même hashcode (vérifiée par assertEquals(s.hashCode(), ese.hashCode());) ET ils sont de la même classe. Toutefois, s et ese sont MyString cas dans ce test, pas de Java String des cas, avec la seule différence pertinente pour ce test étant l'égale: String s equals String ese dans le test ci-dessus, tandis que MyStrings s does not equal MyString ese dans le test de 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 à l'inverse de ce qu'il a dit plus tôt, et reconnaît l'importance d'égal à égal. Cependant, il semble encore la notion d'égale que c'est ce qui importe, pas la "même catégorie", n'est pas claire (l'emphase est mienne):

"Pas vraiment. La liste est créée que si le hash est le même, mais la clé est différente. Par exemple, si une Chaîne de caractères donner hashcode 2345 et Entier et donne le même hashcode 2345, alors l'entier est inséré dans la liste parce que la Chaîne.equals( Entier ) est faux. Mais si vous avez la même classe ( ou au moins .equals vrai ) alors la même entrée est utilisée. Par exemple, new String("un") et " new String("un") utilisés comme des clés, utiliser la même entrée. En fait ce qui est le point de l'ENSEMBLE de la table de hachage en premier lieu! Voyez vous-même: pastebin.com/f20af40b9 – Oscar de los Reyes"

rapport au commentaire précédent qui traitent explicitement de l'importance à l'identique de la classe et même hashcode, sans mention d'égal à égal:

"@delfuego: Voir par vous-même: pastebin.com/f20af40b9 Donc, dans cette question de la même classe est utilisé ( attendez une minute, dans la même classe est utilisé à droite? ) Ce qui implique que lorsque la même valeur de hachage est utilisée de la même entrée est utilisée et il n'y a pas de "liste" d'entrées. – Oscar De Los Reyes"

ou

"En fait, ce serait d'augmenter les performances. Le plus de collisions eq moins d'entrées dans la table de hachage eq. moins de travail à faire. N'est pas le hachage ( qui a l'air bien ), ni la table de hachage ( qui fonctionne très bien ), je parierais que c'est sur la création de l'objet où la performance est dégradant. – Oscar De Los Reyes"

ou

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

Encore une fois, je peut mal comprendre ce que Oscar était en fait en train de dire. Cependant, ses commentaires d'origine ont causé assez de confusion qu'il semble prudent de tout clair avec quelques explicite les tests, donc il n'y a pas de doutes qui subsistent.


[1] - De Efficace Java, Deuxième Édition par Joshua Bloch:

  • Chaque fois qu'elle est invoquée sur le même objet plus d'une fois au cours de l'exécution d'une application, la méthode hashCode doit constamment revenir l' même entier, pas fourni d'information utilisés dans l'égalité de s des comparaisons sur les l'objet est modifié. Cet entier n'a pas besoin de rester cohérent à partir d'une exécution d'une application à une autre exécution de la même application.

  • Si deux objets sont égaux selon l'égalité s(Obj ect) méthode, puis en appelant la méthode hashCode sur chacun des deux objets doivent produire les mêmes résultat sous forme d'entier.

  • Il n'est pas nécessaire que si deux objets sont inégales selon l'égalité s(Objet) de la méthode, puis en appelant la méthode hashCode sur chacun des deux objets doit produire integer distinctes résultats. Toutefois, le programmeur doit être conscient que la production d'un integer distinctes résultats de l'inégalité des objets peut améliorer la performance de tables de hachage.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X