JavaScript à la perte de précision numérique C #

16

Lors de la sérialisation et de la désérialisation des valeurs entre JavaScript et C # à l'aide de SignalR avec MessagePack, je constate un peu de perte de précision en C # à la réception.

Par exemple, j'envoie la valeur 0,005 de JavaScript à C #. Lorsque la valeur désérialisée apparaît du côté C #, j'obtiens la valeur 0.004999999888241291, qui est proche, mais pas 0,005 exactement. La valeur du côté JavaScript est Numberet du côté C # que j'utilise double.

J'ai lu que JavaScript ne peut pas représenter exactement des nombres à virgule flottante, ce qui peut conduire à des résultats comme 0.1 + 0.2 == 0.30000000000000004. Je soupçonne que le problème que je vois est lié à cette fonctionnalité de JavaScript.

La partie intéressante est que je ne vois pas le même problème aller dans l'autre sens. L'envoi de 0,005 de C # vers JavaScript entraîne la valeur 0,005 en JavaScript.

Edit : La valeur de C # est juste raccourcie dans la fenêtre du débogueur JS. Comme @Pete l'a mentionné, il s'étend à quelque chose qui n'est pas exactement 0,5 (0,005000000000000000104083408558). Cela signifie que l'écart se produit au moins des deux côtés.

La sérialisation JSON n'a pas le même problème car je suppose qu'elle passe par une chaîne qui laisse l'environnement récepteur sous contrôle en analysant la valeur dans son type numérique natif.

Je me demande s'il existe un moyen d'utiliser la sérialisation binaire pour avoir des valeurs correspondantes des deux côtés.

Sinon, cela signifie-t-il qu'il n'y a aucun moyen d'avoir des conversions binaires 100% précises entre JavaScript et C #?

Technologie utilisée:

  • Javascript
  • .Net Core avec SignalR et msgpack5

Mon code est basé sur ce post . La seule différence est que j'utilise ContractlessStandardResolver.Instance.

TGH
la source
La représentation en virgule flottante en C # n'est pas également exacte pour chaque valeur. Jetez un œil aux données sérialisées. Comment l'analyser en C #?
JeffRSon
Quel type utilisez-vous en C #? Double est connu pour avoir un tel problème.
Poul Bak
J'utilise la sérénisation / désérialisation de pack de messages intégrée qui vient avec signalr et son intégration de pack de messages.
TGH
Les valeurs en virgule flottante ne sont jamais précises. Si vous avez besoin de valeurs précises, utilisez des chaînes (problème de formatage) ou des entiers (par exemple en multipliant par 1000).
Atmin
Pouvez-vous vérifier le message désérialisé? Le texte que vous avez obtenu de js, avant que c # ne se transforme en objet.
Jonny Piazzi

Réponses:

9

MISE À JOUR

Ce problème a été corrigé dans la prochaine version (5.0.0-preview4) .

Réponse originale

J'ai testé floatet double, et c'est intéressant dans ce cas particulier, je n'ai doubleeu que le problème, alors qu'il floatsemble fonctionner (c'est-à-dire que 0,005 est lu sur le serveur).

L'inspection des octets de message a suggéré que 0,005 est envoyé en tant que type Float32Doublequi est un nombre à virgule flottante à simple précision IEEE 754 de 4 octets / 32 bits bien qu'il Numbersoit de 64 bits à virgule flottante.

Exécutez le code suivant dans la console a confirmé ce qui précède:

msgpack5().encode(Number(0.005))

// Output
Uint8Array(5) [202, 59, 163, 215, 10]

mspack5 fournit une option pour forcer la virgule flottante 64 bits:

msgpack5({forceFloat64:true}).encode(Number(0.005))

// Output
Uint8Array(9) [203, 63, 116, 122, 225, 71, 174, 20, 123]

Cependant, l' forceFloat64option n'est pas utilisée par signalr-protocol-msgpack .

Bien que cela explique pourquoi floatfonctionne côté serveur, mais il n'y a pas vraiment de correctif pour le moment . Attendons ce que dit Microsoft .

Solutions de contournement possibles

  • Hacker les options de msgpack5? Forkez et compilez votre propre msgpack5 avec la forceFloat64valeur par défaut true? Je ne sais pas.
  • Passer du floatcôté serveur
  • Utilisation des stringdeux côtés
  • Basculez vers decimalcôté serveur et écrivez personnalisé IFormatterProvider. decimaln'est pas de type primitif et IFormatterProvider<decimal>est appelé pour des propriétés de type complexes
  • Fournir une méthode pour récupérer la doublevaleur de la propriété et faire l' astuce double-> float-> decimal->double
  • D'autres solutions irréalistes auxquelles vous pourriez penser

TL; DR

Le problème avec le client JS envoyant un seul nombre à virgule flottante au backend C # provoque un problème de virgule flottante connu:

// value = 0.00499999988824129, crazy C# :)
var value = (double)0.005f;

Pour les utilisations directes des doubleméthodes in, le problème peut être résolu par une personnalisation MessagePack.IFormatterResolver:

public class MyDoubleFormatterResolver : IFormatterResolver
{
    public static MyDoubleFormatterResolver Instance = new MyDoubleFormatterResolver();

    private MyDoubleFormatterResolver()
    { }

    public IMessagePackFormatter<T> GetFormatter<T>()
    {
        return MyDoubleFormatter.Instance as IMessagePackFormatter<T>;
    }
}

public sealed class MyDoubleFormatter : IMessagePackFormatter<double>, IMessagePackFormatter
{
    public static readonly MyDoubleFormatter Instance = new MyDoubleFormatter();

    private MyDoubleFormatter()
    {
    }

    public int Serialize(
        ref byte[] bytes,
        int offset,
        double value,
        IFormatterResolver formatterResolver)
    {
        return MessagePackBinary.WriteDouble(ref bytes, offset, value);
    }

    public double Deserialize(
        byte[] bytes,
        int offset,
        IFormatterResolver formatterResolver,
        out int readSize)
    {
        double value;
        if (bytes[offset] == 0xca)
        {
            // 4 bytes single
            // cast to decimal then double will fix precision issue
            value = (double)(decimal)MessagePackBinary.ReadSingle(bytes, offset, out readSize);
            return value;
        }

        value = MessagePackBinary.ReadDouble(bytes, offset, out readSize);
        return value;
    }
}

Et utilisez le résolveur:

services.AddSignalR()
    .AddMessagePackProtocol(options =>
    {
        options.FormatterResolvers = new List<MessagePack.IFormatterResolver>()
        {
            MyDoubleFormatterResolver.Instance,
            ContractlessStandardResolver.Instance,
        };
    });

Le résolveur n'est pas parfait, car couler jusque- decimallà pour doubleralentir le processus et cela pourrait être dangereux .

toutefois

Comme l'OP l'a souligné dans les commentaires, cela ne peut pas résoudre le problème si vous utilisez des types complexes ayant des doublepropriétés de retour.

Une enquête plus approfondie a révélé la cause du problème dans MessagePack-CSharp:

// Type: MessagePack.MessagePackBinary
// Assembly: MessagePack, Version=1.9.0.0, Culture=neutral, PublicKeyToken=b4a0369545f0a1be
// MVID: B72E7BA0-FA95-4EB9-9083-858959938BCE
// Assembly location: ...\.nuget\packages\messagepack\1.9.11\lib\netstandard2.0\MessagePack.dll

namespace MessagePack.Decoders
{
  internal sealed class Float32Double : IDoubleDecoder
  {
    internal static readonly IDoubleDecoder Instance = (IDoubleDecoder) new Float32Double();

    private Float32Double()
    {
    }

    public double Read(byte[] bytes, int offset, out int readSize)
    {
      readSize = 5;
      // The problem is here
      // Cast a float value to double like this causes precision loss
      return (double) new Float32Bits(bytes, checked (offset + 1)).Value;
    }
  }
}

Le décodeur ci-dessus est utilisé pour convertir un seul floatnombre en double:

// From MessagePackBinary class
MessagePackBinary.doubleDecoders[202] = Float32Double.Instance;

v2

Ce problème existe dans les versions v2 de MessagePack-CSharp. J'ai déposé un problème sur github , bien que le problème ne soit pas résolu .

weichch
la source
Résultats intéressants. Un défi ici est que le problème s'applique à n'importe quel nombre de propriétés doubles sur un objet complexe, donc il sera difficile de cibler directement le double je pense.
TGH
@TGH Ouais, tu as raison. Je pense que c'est un bogue dans MessagePack-CSharp. Voir ma mise à jour pour plus de détails. Pour l'instant, vous devrez peut-être utiliser floatcette solution. Je ne sais pas s'ils ont corrigé cela en v2. Je jetterai un coup d'œil une fois que j'aurai du temps. Cependant, le problème est que la v2 n'est pas encore compatible avec SignalR. Seules les versions d'aperçu (5.0.0.0- *) de SignalR peuvent utiliser la v2.
weichch
Cela ne fonctionne pas non plus en v2. J'ai soulevé un bogue avec MessagePack-CSharp.
weichch
@TGH Malheureusement, il n'y a pas de correctif côté serveur selon la discussion dans le problème github. La meilleure solution serait d'obtenir que le client envoie 64 bits au lieu de 32 bits. J'ai remarqué qu'il existe une option pour forcer cela à se produire, mais Microsoft ne l'expose pas (à ma connaissance). Réponse juste mise à jour avec quelques solutions de contournement désagréables si vous voulez y jeter un œil. Et bonne chance sur cette question.
weichch
Cela ressemble à une piste intéressante. Je vais y jeter un œil. Merci pour votre aide!
TGH
14

Veuillez vérifier la valeur précise que vous envoyez avec une plus grande précision. Les langues limitent généralement la précision de l'impression pour la rendre plus belle.

var n = Number(0.005);
console.log(n);
0.005
console.log(n.toPrecision(100));
0.00500000000000000010408340855860842566471546888351440429687500000000...
Pete
la source
Oui, vous avez raison à ce sujet.
TGH