web-dev-qa-db-fra.com

L'opérateur modulo (%) donne un résultat différent pour les différentes versions de .NET en C #

Je crypte l'entrée de l'utilisateur pour générer une chaîne de mot de passe. Mais une ligne de code donne des résultats différents dans différentes versions du framework. Code partiel avec la valeur de la touche enfoncée par l'utilisateur:

Touche enfoncée: 1. La variable ascii est 49. Valeur de 'e' et 'n' après un certain calcul:

e = 103, 
n = 143,

Math.Pow(ascii, e) % n

Résultat du code ci-dessus:

  • Dans .NET 3.5 (C #)

    Math.Pow(ascii, e) % n
    

    donne 9.0.

  • Dans .NET 4 (C #)

    Math.Pow(ascii, e) % n
    

    donne 77.0.

Math.Pow() donne le résultat correct (identique) dans les deux versions.

Quelle en est la cause et existe-t-il une solution?

89
Rajiv Bhardwaj

Math.Pow fonctionne sur les nombres à virgule flottante double précision; ainsi, vous ne devez pas vous attendre à plus que les 15 à 17 premiers chiffres du résultat pour être précis:

Tous les nombres à virgule flottante ont également un nombre limité de chiffres significatifs, ce qui détermine également la précision avec laquelle une valeur à virgule flottante se rapproche d'un nombre réel. Une valeur Double a jusqu'à 15 chiffres décimaux de précision, bien qu'un maximum de 17 chiffres soit conservé en interne.

Cependant, l'arithmétique modulo nécessite que tous les chiffres soient précis. Dans votre cas, vous calculez 49103, dont le résultat est composé de 175 chiffres, rendant l'opération modulo vide de sens dans vos deux réponses.

Pour déterminer la valeur correcte, vous devez utiliser une arithmétique de précision arbitraire, telle que fournie par la classe BigInteger (introduite dans .NET 4.0).

int val = (int)(BigInteger.Pow(49, 103) % 143);   // gives 114

Edit : Comme l'a souligné Mark Peters dans les commentaires ci-dessous, vous devez utiliser le BigInteger.ModPow , qui est destinée spécifiquement à ce type d'opération:

int val = (int)BigInteger.ModPow(49, 103, 143);   // gives 114
160
Douglas

Mis à part le fait que votre fonction de hachage n'est pas très bonne *, le plus gros problème avec votre code n'est pas qu'il renvoie un nombre différent en fonction de la version de .NET, mais que dans les deux cas, il renvoie un nombre entièrement dénué de sens: la bonne réponse au problème est

49103 mod 143 = est 114. ( lien vers Wolfram Alpha )

Vous pouvez utiliser ce code pour calculer cette réponse:

private static int PowMod(int a, int b, int mod) {
    if (b == 0) {
        return 1;
    }
    var tmp = PowMod(a, b/2, mod);
    tmp *= tmp;
    if (b%2 != 0) {
        tmp *= a;
    }
    return tmp%mod;
}

La raison pour laquelle votre calcul produit un résultat différent est que, pour produire une réponse, vous utilisez une valeur intermédiaire qui supprime la plupart des chiffres significatifs des 49103 nombre: seuls les 16 premiers de ses 175 chiffres sont corrects!

1230824813134842807283798520430636310264067713738977819859474030746648511411697029659004340261471771152928833391663821316264359104254030819694748088798262075483562075061997649

Les 159 chiffres restants sont tous erronés. Cependant, l'opération de mod recherche un résultat qui nécessite que chaque chiffre soit correct, y compris les derniers. Par conséquent, même la plus petite amélioration de la précision de Math.Pow qui peut avoir été implémenté dans .NET 4, entraînerait une différence drastique de votre calcul, ce qui produit essentiellement un résultat arbitraire.

* Puisque cette question parle d'élever des entiers à des puissances élevées dans le contexte du hachage de mot de passe, il peut être une très bonne idée de lire cette réponselien avant de décider si votre approche actuelle doit être changée pour une potentiellement meilleure.

72
dasblinkenlight

Ce que vous voyez est une erreur d'arrondi en double. Math.Pow Fonctionne avec double et la différence est la suivante:

.NET 2.0 et 3.5 => var powerResult = Math.Pow(ascii, e); renvoie:

1.2308248131348429E+174

.NET 4.0 et 4.5 => var powerResult = Math.Pow(ascii, e); renvoie:

1.2308248131348427E+174

Remarquez le dernier chiffre avant E et cela cause la différence dans le résultat. Ce n'est pas l'opérateur du module(%).

27
Habib

La précision en virgule flottante peut varier d'une machine à l'autre, et même sur la même machine .

Cependant, le .NET crée une machine virtuelle pour vos applications ... mais il y a des changements de version en version.

Par conséquent, vous ne devez pas vous y fier pour produire des résultats cohérents. Pour le chiffrement, utilisez les classes fournies par Framework plutôt que de rouler les vôtres.

24
Joe

Il y a beaucoup de réponses sur la façon dont le code est mauvais. Cependant, pourquoi le résultat est différent…

Les FPU d'Intel utilisent le format 80 bits en interne pour obtenir plus de précision pour les résultats intermédiaires. Donc, si une valeur est dans le registre du processeur, elle obtient 80 bits, mais lorsqu'elle est écrite dans la pile, elle est stockée à 64 bits .

Je m'attends à ce que la nouvelle version de .NET ait un meilleur optimiseur dans sa compilation Just in Time (JIT), donc il garde une valeur dans un registre plutôt que de l'écrire dans la pile et de la relire à partir de la pile.

Il se peut que le JIT puisse maintenant retourner une valeur dans un registre plutôt que sur la pile. Ou passez la valeur à la fonction MOD dans un registre.

Voir aussi la question Stack Overflow Quels sont les applications/avantages d'un type de données à précision étendue 80 bits?

Autres processeurs, par exemple le ARM donnera des résultats différents pour ce code.

10
Ian Ringrose

Il est peut-être préférable de le calculer vous-même en utilisant uniquement l'arithmétique entière. Quelque chose comme:

int n = 143;
int e = 103;
int result = 1;
int ascii = (int) 'a';

for (i = 0; i < e; ++i) 
    result = result * ascii % n;

Vous pouvez comparer les performances avec les performances de la solution BigInteger publiées dans les autres réponses.

6
Ronald