JavaScript a C # Pérdida de precisión numérica

16

Al serializar y deserializar valores entre JavaScript y C # usando SignalR con MessagePack, veo un poco de pérdida de precisión en C # en el extremo receptor.

Como ejemplo, estoy enviando el valor 0.005 de JavaScript a C #. Cuando el valor deserializado aparece en el lado de C # obtengo el valor 0.004999999888241291, que está cerca, pero no exactamente 0.005. El valor en el lado de JavaScript es Numbery en el lado de C # que estoy usando double.

He leído que JavaScript no puede representar números de coma flotante exactamente, lo que puede conducir a resultados como 0.1 + 0.2 == 0.30000000000000004. Sospecho que el problema que estoy viendo está relacionado con esta característica de JavaScript.

Lo interesante es que no veo el mismo problema al revés. Enviar 0.005 de C # a JavaScript da como resultado el valor 0.005 en JavaScript.

Editar : el valor de C # se acorta en la ventana del depurador JS. Como @Pete mencionó, se expande a algo que no es 0.5 exactamente (0.005000000000000000104083408558). Esto significa que la discrepancia ocurre en ambos lados al menos.

La serialización JSON no tiene el mismo problema, ya que supongo que va a través de una cadena que deja el entorno de recepción en control wrt analizando el valor en su tipo numérico nativo.

Me pregunto si hay una forma de usar la serialización binaria para tener valores coincidentes en ambos lados.

Si no, ¿significa esto que no hay forma de tener conversiones binarias 100% precisas entre JavaScript y C #?

Tecnología utilizada:

  • JavaScript
  • .Net Core con SignalR y msgpack5

Mi código se basa en esta publicación . La única diferencia es que estoy usando ContractlessStandardResolver.Instance.

TGH
fuente
La representación de punto flotante en C # tampoco es exacta para cada valor. Echa un vistazo a los datos serializados. ¿Cómo se analiza en C #?
JeffRSon
¿Qué tipo usas en C #? Se sabe que Double tiene ese problema.
Poul Bak
Uso la serialización / deserialización del paquete de mensajes incorporada que viene con el señalizador y es la integración del paquete de mensajes.
TGH
Los valores de coma flotante nunca son precisos. Si necesita valores precisos, use cadenas (problema de formato) o enteros (por ejemplo, multiplicando por 1000).
atmin
¿Puedes consultar el mensaje deserializado? El texto que obtuvo de js, antes de que c # se convierta en un objeto.
Jonny Piazzi

Respuestas:

9

ACTUALIZAR

Esto se ha solucionado en la próxima versión (5.0.0-preview4) .

Respuesta original

Lo probé floaty double, curiosamente en este caso particular, solo doubletuve el problema, mientras que floatparece estar funcionando (es decir, 0.005 se lee en el servidor).

La inspección en los bytes del mensaje sugirió que 0.005 se envía como tipo, Float32Doubleque es un número de coma flotante de precisión simple IEEE 754 de 4 bytes / 32 bits a pesar de Numberser un punto flotante de 64 bits.

Ejecute el siguiente código en la consola confirmó lo anterior:

msgpack5().encode(Number(0.005))

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

mspack5 proporciona una opción para forzar el punto flotante de 64 bits:

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

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

Sin embargo, la forceFloat64opción no es utilizada por signalr-protocol-msgpack .

Aunque eso explica por qué floatfunciona en el lado del servidor, pero no hay realmente una solución para eso a partir de ahora . Esperemos lo que dice Microsoft .

Posibles soluciones

  • Hackear las opciones de msgpack5? Bifurca y compila tu propio msgpack5 con el forceFloat64valor predeterminado verdadero? No lo sé.
  • Cambiar al floatlado del servidor
  • Usar stringen ambos lados
  • Cambie al decimallado del servidor y escriba personalizado IFormatterProvider. decimalno es de tipo primitivo y IFormatterProvider<decimal>se llama para propiedades de tipo complejo
  • Proporcione un método para recuperar el doublevalor de la propiedad y haga el truco double-> float-> decimal->double
  • Otras soluciones poco realistas que se te ocurran

TL; DR

El problema con el cliente JS que envía un único número de coma flotante al backend de C # causa un problema conocido de coma flotante:

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

Para usos directos de los doublemétodos in, el problema podría resolverse mediante una costumbre 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;
    }
}

Y usa el resolutor:

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

El resolutor no es perfecto, ya que lanzarlo a decimalcontinuación doubleralentiza el proceso y podría ser peligroso .

sin embargo

Según el OP señalado en los comentarios, esto no puede resolver el problema si se utilizan tipos complejos con doublepropiedades de retorno.

La investigación adicional reveló la causa del problema en 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;
    }
  }
}

El decodificador anterior se usa cuando se necesita convertir un solo floatnúmero a double:

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

v2

Este problema existe en las versiones v2 de MessagePack-CSharp. He presentado un problema en github , aunque el problema no se solucionará .

Weichch
fuente
Hallazgos interesantes. Un desafío aquí es que el problema se aplica a cualquier número de propiedades dobles en un objeto complejo, por lo que creo que será difícil apuntar al doble directamente.
TGH
@TGH Sí, tienes razón. Creo que es un error en MessagePack-CSharp. Ver mi actualizado para más detalles. Por ahora, es posible que deba usarlo floatcomo solución alternativa. No sé si lo arreglaron en v2. Echaré un vistazo una vez que tenga algo de tiempo. Sin embargo, el problema es que v2 aún no es compatible con SignalR. Solo las versiones de vista previa (5.0.0.0- *) de SignalR pueden usar v2.
weichch
Esto tampoco funciona en v2. He planteado un error con MessagePack-CSharp.
Weichch
@TGH Desafortunadamente, no hay ninguna solución en el lado del servidor según la discusión en el tema de github. La mejor solución sería hacer que el lado del cliente envíe 64 bits en lugar de 32 bits. Noté que hay una opción para forzar que eso suceda, pero Microsoft no lo expone (según tengo entendido). Acabo de actualizar la respuesta con algunas soluciones desagradables si quieres echar un vistazo. Y buena suerte en este tema.
weichch
Eso suena como una pista interesante. Echaré un vistazo a eso. ¡Gracias por ayudarme con esto!
TGH
14

Verifique el valor exacto que está enviando con mayor precisión. Los idiomas generalmente limitan la precisión en la impresión para que se vea mejor.

var n = Number(0.005);
console.log(n);
0.005
console.log(n.toPrecision(100));
0.00500000000000000010408340855860842566471546888351440429687500000000...
Pete
fuente
Sí, tienes razón en eso.
TGH