web-dev-qa-db-fra.com

Pourquoi le hashCode () de String ne cache-t-il pas 0?

J'ai remarqué dans le code source Java 6 de String que hashCode ne met en cache que des valeurs autres que 0. La différence de performances est illustrée par l'extrait suivant:

public class Main{
   static void test(String s) {
      long start = System.currentTimeMillis();
      for (int i = 0; i < 10000000; i++) {
         s.hashCode();
      }
      System.out.format("Took %d ms.%n", System.currentTimeMillis() - start);
   }
   public static void main(String[] args) {
      String z = "Allocator redistricts; strict allocator redistricts strictly.";
      test(z);
      test(z.toUpperCase());
   }
}

Lancer ceci dans ideone.com donne le résultat suivant:

Took 1470 ms.
Took 58 ms.

Donc mes questions sont:

  • Pourquoi le hashCode () de String ne cache-t-il pas 0?
  • Quelle est la probabilité qu'une chaîne Java se hache à 0?
  • Quel est le meilleur moyen d'éviter la pénalité de performance consistant à recalculer la valeur de hachage à chaque fois pour des chaînes dont le hachage est 0?
  • Est-ce la meilleure façon de mettre en cache des valeurs? (cachez-vous tous sauf un?)

Pour votre amusement, chaque ligne ici est une chaîne qui se hache à 0:

pollinating sandboxes
amusement & hemophilias
schoolworks = perversive
electrolysissweeteners.net
constitutionalunstableness.net
grinnerslaphappier.org
BLEACHINGFEMININELY.NET
WWW.BUMRACEGOERS.ORG
WWW.RACCOONPRUDENTIALS.NET
Microcomputers: the unredeemed Lollipop...
Incentively, my dear, I don't tessellate a derangement.
A person who never yodelled an apology, never preened vocalizing transsexuals.
71
polygenelubricants

Tu t'inquiètes pour rien. Voici une façon de penser à ce problème.

Supposons que vous ayez une application qui ne fait rien que rester assis autour du hachage des chaînes tout au long de l'année. Disons que cela prend un millier de chaînes, toutes en mémoire, appelle hashCode () dessus à plusieurs reprises en mode alterné, un million de fois, puis obtient encore un millier de nouvelles chaînes et le fait à nouveau.

Et supposons que la probabilité que le code de hachage d'une chaîne soit nul est en réalité beaucoup plus grande que 1/2 ^ 32. Je suis sûr que c'est un peu supérieur à 1/2 ^ 32, mais disons que c'est bien pire que ça, comme 1/2 ^ 16 (la racine carrée! Maintenant c'est bien pire!).

Dans cette situation, les ingénieurs d'Oracle ont mieux à faire pour améliorer la mise en cache des codes de hachage de ces chaînes que quiconque. Alors vous leur écrivez et leur demandez de le réparer. Et ils travaillent leur magie pour que chaque fois que s.hashCode () vaut zéro, il retourne instantanément (même la première fois! Une amélioration de 100%!). Et disons qu'ils le font sans dégrader la performance pour aucun autre cas.

Hourra! Maintenant, votre application est ... voyons ... 0,0015% plus vite!

Ce qui prenait une journée entière ne prend plus que 23 heures, 57 minutes et 48 secondes!

Et rappelez-vous, nous avons mis en place le scénario afin de donner tous les avantages possibles du doute, souvent à un degré ridicule.

Cela vous semble-t-il valoir la peine?

EDIT: Depuis que j'ai posté cela il y a quelques heures, j'ai laissé l'un de mes processeurs courir à la recherche de phrases à deux mots avec zéro code de hachage. Jusqu’à présent, c’est ce que nous avons inventé: bequirtle zorillo, chronogramme schtoff, cloître contusif, organzine de creashaks, tête de roche drumwood, exercice électroanalytique, et incroyablement non transformable. Ceci est sur environ 2 ^ 35 possibilités, donc avec une distribution parfaite, nous nous attendrions à n'en voir que 8. Clairement, à la fin, nous en aurons quelques fois plus, mais pas plus bizarrement. Ce qui est plus important, c'est que j'ai maintenant quelques noms intéressants de groupes/noms d'albums! Pas juste de voler!

55
Kevin Bourrillion

Il utilise 0 pour indiquer "Je n'ai pas encore élaboré le hashcode". L'alternative serait d'utiliser un indicateur booléen séparé, ce qui prendrait plus de mémoire. (Ou bien ne pas mettre le hashcode en cache, bien sûr.)

Je ne m'attends pas à many strings hash to 0; sans aucun doute, il serait logique que la routine de hachage évite délibérément 0 (par exemple, traduire un hachage de 0 à 1 et le mettre en cache). Cela augmenterait les collisions mais éviterait de ressasser. Cependant, il est trop tard pour le faire, car l'algorithme String hashCode est explicitement documenté.

Quant à savoir s’il s’agit d’une bonne idée en général: c’est un mécanisme de mise en cache certainement efficace, et peut-être (voir modification), ce qui est encore mieux avec une modification permettant d’éviter de reprendre les valeurs qui se terminent par un hachage de 0. Je serais intéressé de voir les données qui ont amené Sun à croire que cela en valait la peine en premier lieu - cela prend 4 octets supplémentaires pour chaque chaîne jamais créée, qu'elle soit souvent ou rarement hachée, et le seul avantage est celui des chaînes qui sont hachés plus d'une fois .

EDIT: Comme KevinB l’a souligné dans un commentaire ailleurs, la suggestion "éviter 0" ci-dessus pourrait bien avoir un net coût car cela aide un très rare cas, mais nécessite une comparaison supplémentaire pour tous les calcul de hachage.

24
Jon Skeet

Je pense qu'il manque quelque chose d'important aux autres réponses jusqu'à présent: la valeur zéro existe pour que le mécanisme de mise en cache de hashCode fonctionne de manière robuste dans un environnement multithread.

Si vous aviez deux variables, comme cachedHashCode et un booléen isHashCodeCalculated pour indiquer si cachedHashCode avait été calculé, vous auriez besoin d'une synchronisation de threads pour que les éléments fonctionnent dans un environnement multithread. Et la synchronisation serait mauvaise pour les performances, d’autant plus que les chaînes sont très souvent réutilisées dans plusieurs threads.

Ma compréhension du modèle de mémoire Java est un peu sommaire, mais voici en gros ce qui se passe:

  1. Lorsque plusieurs threads accèdent à une variable (comme le hashCode mis en cache), rien ne garantit que chaque thread verra la dernière valeur. Si une variable commence à zéro, alors A la met à jour (la définit sur une valeur autre que zéro), puis le fil B la lit peu de temps après, le fil B peut toujours voir la valeur zéro.

  2. Il existe un autre problème lié à l'accès aux valeurs partagées de plusieurs threads (sans synchronisation): vous pouvez éventuellement essayer d'utiliser un objet qui n'a été que partiellement initialisé (la construction d'un objet n'est pas un processus atomique). Les lectures et écritures multithreads de primitives 64 bits telles que les longs et doubles ne sont pas nécessairement atomiques non plus, donc si deux threads tentent de lire et de modifier la valeur d'un long ou d'un double, un thread peut finir par voir quelque chose de bizarre et partiellement défini . Ou quelque chose comme ça quand même. Si vous essayez d'utiliser simultanément deux variables, telles que cachedHashCode et isHashCodeCalculated, des problèmes similaires se présentent. Un thread peut facilement afficher la dernière version de l'une de ces variables, mais une version antérieure de l'autre.

  3. Le moyen habituel de contourner ces problèmes multi-threading consiste à utiliser la synchronisation. Par exemple, vous pouvez placer tous les accès au hashCode mis en cache à l'intérieur d'un bloc synchronisé, ou vous pouvez utiliser le mot clé volatile (même si vous devez être prudent, la sémantique est un peu déroutante).

  4. Cependant, la synchronisation ralentit les choses. Mauvaise idée pour quelque chose comme une chaîne hashCode. Les chaînes sont très souvent utilisées comme clés dans HashMaps, vous avez donc besoin de la méthode hashCode pour fonctionner correctement, y compris dans les environnements multithreads.

  5. Les primitives Java 32 bits ou moins, comme int, sont spéciales. Contrairement à, par exemple, une valeur longue (valeur de 64 bits), vous pouvez être sûr de ne jamais lire une valeur partiellement initialisée d'un int (32 bits). Lorsque vous lisez un int sans synchronisation, vous ne pouvez pas être sûr d'obtenir la dernière valeur définie, mais vous pouvez être sûr que la valeur que vous obtenez est une valeur qui a été explicitement définie à un moment donné par votre thread ou un autre fil.

Le mécanisme de mise en cache hashCode dans Java.lang.String est configuré pour s'appuyer sur le point 5 ci-dessus. Vous pourriez mieux le comprendre en regardant la source de Java.lang.String.hashCode (). Fondamentalement, lorsque plusieurs threads appellent hashCode en même temps, hashCode peut être calculé plusieurs fois (soit si la valeur calculée est zéro, soit si plusieurs threads appellent hashCode en même temps et que les deux voient une valeur mise en cache nulle), mais vous pouvez être sûr que hashCode () retournera toujours la même valeur. Donc, il est robuste et performant aussi (car il n’ya pas de synchronisation pouvant faire office de goulet d’étranglement dans les environnements multithreads).

Comme je l'ai dit, ma compréhension du modèle de mémoire Java est un peu sommaire, mais je suis à peu près sûr que j'ai le bon sens de ce qui précède. En fin de compte, c'est un langage très intelligent pour mettre en cache le hashCode sans la surcharge de la synchronisation.

18
MB.

0 n'est pas mis en cache car l'implémentation interprète une valeur mise en cache de 0 comme "valeur mise en cache non encore initialisée". L’alternative aurait été d’utiliser un Java.lang.Integer, où null impliquerait que la valeur n’était pas encore mise en cache. Cependant, cela aurait entraîné une surcharge de stockage.

En ce qui concerne la probabilité que le code de hachage d'un String soit calculé à 0, je dirais que la probabilité est assez faible et peut se produire dans les cas suivants:

  • La chaîne est vide (bien que recalculer ce code de hachage à chaque fois est effectivement O (1)).
  • Un débordement se produit lorsque le code de hachage calculé final est 0 (e.g. Integer.MAX_VALUE + h(c1) + h(c2) + ... h(cn) == 0).
  • La chaîne ne contient que le caractère Unicode 0. Très peu probable car il s'agit d'un caractère de contrôle sans signification en dehors du "monde de la bande de papier" (!):

De Wikipedia :

Le code 0 (nom de code ASCII NUL) est un cas particulier . Dans une bande de papier, c'est le cas Lorsqu'il n'y a pas de trous. Il est Pratique de traiter cela comme un caractère de remplissage Caractère sans autre signification .

8
Adamski

Cela s'avère être une bonne question, liée à une vulnérabilité de sécurité .

"Lors du hachage d'une chaîne, Java met également en cache la valeur de hachage Dans l'attribut" hash ", mais uniquement si le résultat est différent de zéro. il empêche la mise en cache et force le re-hachage. "

5
cdunn2001

Eh bien les gens, il garde 0 car s'il est de longueur nulle, il finira par être zéro de toute façon.

Et il ne faut pas longtemps pour comprendre que la len est égale à zéro et que le hashcode doit l'être également.

Donc, pour votre code-reviewz! La voici dans toute sa gloire Java 8:

 public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

Comme vous pouvez le constater, cela retournera toujours un zéro rapide si la chaîne est vide:

  if (h == 0 && value.length > 0) ...
0
The Coordinator

La suggestion "eviter 0" semble être recommandée comme meilleure pratique car elle permet de résoudre un problème réel (dégradation sérieuse et inattendue des performances dans des cas constructibles pouvant être fournis par un attaquant) pour le faible coût d’une opération de succursale avant une écriture. Il reste une "dégradation inattendue des performances" qui peut être exercée si les seuls éléments entrant dans un hachage défini correspondent à la valeur ajustée spéciale. Mais il s’agit au pire d’une dégradation 2x plutôt que sans limite.

Bien entendu, l'implémentation de String ne peut pas être modifiée, mais il n'est pas nécessaire de perpétuer le problème.

0
Mike Liddell
  • Pourquoi le hashCode () de String ne cache-t-il pas 0?

La valeur zéro est réservée à "le code de hachage n'est pas mis en cache".

  • Quelle est la probabilité qu'une chaîne Java se hache à 0?

Selon la Javadoc, la formule du hashcode d'une chaîne est la suivante:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

en utilisant l'arithmétique int, où s[i] est le dixième caractère de la chaîne et n est la longueur de la chaîne. (Le hachage de la chaîne vide est défini sur zéro comme cas spécial.)

Mon intuition est que la fonction hashcode comme ci-dessus donne une répartition uniforme des valeurs de hachage String sur la plage de valeurs int. Un écart uniforme signifiant que la probabilité d'un hachage à zéro de la chaîne généré de manière aléatoire était de 1 sur 2 ^ 32.

  • Quel est le meilleur moyen d'éviter la pénalité de performance consistant à recalculer la valeur de hachage à chaque fois pour des chaînes dont le hachage est égal à 0?

La meilleure stratégie consiste à ignorer le problème. Si vous hachez à plusieurs reprises la même valeur String, votre algorithme a quelque chose d'assez étrange.

  • Est-ce la meilleure façon de mettre en cache des valeurs? (cachez-vous tous sauf un?)

C'est un compromis entre l'espace et le temps. Autant que je sache, les alternatives sont:

  • Ajoutez un indicateur cached à chaque objet String, afin que chaque chaîne Java prenne un mot supplémentaire.

  • Utilisez le bit supérieur du membre hash comme indicateur mis en cache. De cette façon, vous pouvez mettre en cache toutes les valeurs de hachage, mais vous n'avez que la moitié du nombre possible de valeurs de hachage String.

  • Ne cachez pas du tout les codes de hachage sur les chaînes.

Je pense que les concepteurs de Java ont fait le bon choix pour Strings et je suis sûr qu’ils ont effectué un profilage approfondi qui confirme la validité de leur décision. Cependant, ne le fait pas, il s'ensuit que ce serait toujours le meilleur moyen de gérer la mise en cache.

(Notez qu’il existe deux valeurs de chaîne "communes" dont le hachage est égal à zéro: la chaîne vide et la chaîne consistant uniquement en un caractère NUL. Toutefois, le coût de calcul des codes de hachage pour ces valeurs est faible par rapport au coût de calcul du hashcode pour une valeur de chaîne typique.)

0
Stephen C