Perdita di precisione numerica da JavaScript a C #


16

Durante la serializzazione e la deserializzazione dei valori tra JavaScript e C # utilizzando SignalR con MessagePack vedo un po 'di perdita di precisione in C # sull'estremità ricevente.

Ad esempio, sto inviando il valore 0.005 da JavaScript a C #. Quando il valore deserializzato appare sul lato C # sto ottenendo il valore 0.004999999888241291, che è vicino, ma non esattamente 0,005. Il valore sul lato JavaScript è Numbere sul lato C # che sto usando double.

Ho letto che JavaScript non può rappresentare esattamente i numeri in virgola mobile che possono portare a risultati simili 0.1 + 0.2 == 0.30000000000000004. Sospetto che il problema che vedo sia correlato a questa funzione di JavaScript.

La parte interessante è che non vedo lo stesso problema andare dall'altra parte. L'invio di 0,005 da C # a JavaScript comporta il valore 0,005 in JavaScript.

Modifica : il valore di C # è stato ridotto nella finestra del debugger JS. Come accennato da @Pete, si espande in qualcosa che non è esattamente 0,5 (0,005000000000000000104083408558). Ciò significa che la discrepanza si verifica almeno da entrambe le parti.

La serializzazione JSON non ha lo stesso problema poiché suppongo che passi attraverso una stringa che lascia il controllo dell'ambiente ricevente sotto controllo analizzando il valore nel suo tipo numerico nativo.

Mi chiedo se c'è un modo usando la serializzazione binaria per avere valori corrispondenti su entrambi i lati.

In caso contrario, significa che non c'è modo di avere conversioni binarie accurate al 100% tra JavaScript e C #?

Tecnologia utilizzata:

  • JavaScript
  • .Net Core con SignalR e msgpack5

Il mio codice si basa su questo post . L'unica differenza è che sto usando ContractlessStandardResolver.Instance.


La rappresentazione in virgola mobile in C # non è esatta anche per ogni valore. Dai un'occhiata ai dati serializzati. Come lo analizzi in C #?
JeffRSon,

Che tipo usi in C #? Double è noto per avere questo problema.
Poul Bak,

Uso la serializzazione / deserializzazione integrata del pacchetto di messaggi fornita con Signalr ed è l'integrazione del pacchetto di messaggi.
TGH,

I valori in virgola mobile non sono mai precisi. Se hai bisogno di valori precisi, usa stringhe (problema di formattazione) o numeri interi (ad es. Moltiplicando per 1000).
atmin

Puoi controllare il messaggio deserializzato? Il testo che hai ricevuto da js, prima che c # converta in un oggetto.
Jonny Piazzi il

Risposte:


9

AGGIORNARE

Questo problema è stato risolto nella prossima versione (5.0.0-preview4) .

Risposta originale

Ho provato floate double, cosa interessante, in questo caso particolare, ho doubleavuto solo il problema, mentre floatsembra funzionare (ovvero 0,005 viene letto sul server).

L'ispezione dei byte di messaggio ha suggerito che 0,005 viene inviato come tipo Float32Doubleche è un numero in virgola mobile IEEE 754 a 4 byte / 32 bit a precisione singola nonostante Numbersia un virgola mobile a 64 bit.

Esegui il seguente codice nella console confermato quanto sopra:

msgpack5().encode(Number(0.005))

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

mspack5 fornisce un'opzione per forzare il virgola mobile a 64 bit:

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

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

Tuttavia, l' forceFloat64opzione non è utilizzata da signalr-protocol-msgpack .

Anche se questo spiega perché floatfunziona sul lato server, ma al momento non esiste una soluzione per questo . Aspettiamo cosa dice Microsoft .

Possibili soluzioni alternative

  • Hack msgpack5 opzioni? Effettua il fork e compila il tuo msgpack5 di forceFloat64default su true ?? Non lo so.
  • Passa al floatlato server
  • Utilizzare stringsu entrambi i lati
  • Passa a decimallato server e scrivi personalizzato IFormatterProvider. decimalnon è un tipo primitivo ed IFormatterProvider<decimal>è chiamato per proprietà di tipo complesso
  • Fornisci il metodo per recuperare il doublevalore della proprietà ed eseguire il trucco double-> float-> decimal->double
  • Altre soluzioni non realistiche a cui potresti pensare

TL; DR

Il problema con il client JS che invia un singolo numero in virgola mobile al back-end C # provoca un problema noto in virgola mobile:

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

Per usi diretti di doublein metodi, il problema potrebbe essere risolto da un'abitudine 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;
    }
}

E usa il resolver:

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

Il resolver non è perfetto, dato che il casting per decimalpoi doublerallentare il processo e potrebbe essere pericoloso .

però

Come indicato dall'OP nei commenti, questo non può risolvere il problema se si utilizzano tipi complessi con doubleproprietà di ritorno.

Ulteriori indagini hanno rivelato la causa del problema in 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;
    }
  }
}

Il decodificatore sopra è utilizzato quando è necessario convertire un singolo floatnumero in double:

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

v2

Questo problema esiste nelle versioni v2 di MessagePack-CSharp. Ho presentato un problema su github , anche se il problema non verrà risolto .


Risultati interessanti. Una sfida qui è che il problema si applica a qualsiasi numero di doppie proprietà su un oggetto complesso, quindi sarà difficile scegliere come target il doppio direttamente, penso.
TGH

@TGH Sì, hai ragione. Credo che sia un bug in MessagePack-CSharp. Vedi il mio aggiornato per i dettagli. Per ora, potrebbe essere necessario utilizzarlo floatcome soluzione alternativa. Non so se l'hanno risolto in v2. Darò un'occhiata una volta che avrò del tempo. Tuttavia, il problema è che v2 non è ancora compatibile con SignalR. Solo le versioni di anteprima (5.0.0.0- *) di SignalR possono utilizzare v2.
Weichch

Questo non funziona neanche in v2. Ho sollevato un bug con MessagePack-CSharp.
Weichch

@TGH Sfortunatamente non ci sono correzioni sul lato server come da discussione in github. La soluzione migliore sarebbe far sì che il lato client invii 64 bit anziché 32 bit. Ho notato che esiste un'opzione per forzare che ciò accada, ma Microsoft non lo espone (dalla mia comprensione). Se vuoi dare un'occhiata, rispondi solo con alcune brutte soluzioni alternative. E buona fortuna per questo problema.
Weichch

Sembra un vantaggio interessante. Lo darò un'occhiata. Grazie per l'aiuto!
TGH

14

Verifica il valore esatto che stai inviando a una precisione maggiore. Le lingue in genere limitano la precisione della stampa per renderla migliore.

var n = Number(0.005);
console.log(n);
0.005
console.log(n.toPrecision(100));
0.00500000000000000010408340855860842566471546888351440429687500000000...

Sì, hai ragione.
TGH,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.