Perda de precisão numérica de JavaScript para C #

16

Ao serializar e desserializar valores entre JavaScript e C # usando o SignalR com MessagePack, estou vendo um pouco de perda de precisão em C # no lado de recebimento.

Como exemplo, estou enviando o valor 0,005 do JavaScript para o C #. Quando o valor desserializado aparece no lado do C #, estou recebendo o valor 0.004999999888241291, que está próximo, mas não exatamente 0,005. O valor no lado JavaScript é Numbere no lado C # que estou usando double.

Eu li que o JavaScript não pode representar números de ponto flutuante exatamente o que pode levar a resultados como 0.1 + 0.2 == 0.30000000000000004. Suspeito que o problema que estou vendo esteja relacionado a esse recurso do JavaScript.

A parte interessante é que não estou vendo o mesmo problema no outro sentido. Enviar 0,005 de C # para JavaScript resulta no valor 0,005 em JavaScript.

Edit : O valor de C # é apenas reduzido na janela do depurador JS. Como o @Pete mencionou, ele se expande para algo que não é exatamente 0,5 (0,005000000000000000104083408558). Isso significa que a discrepância acontece nos dois lados, pelo menos.

A serialização JSON não tem o mesmo problema, pois estou assumindo que ela passa por uma string que deixa o ambiente de recebimento no controle wrt, analisando o valor em seu tipo numérico nativo.

Gostaria de saber se existe uma maneira de usar a serialização binária para ter valores correspondentes em ambos os lados.

Caso contrário, isso significa que não há como ter conversões binárias 100% precisas entre JavaScript e C #?

Tecnologia utilizada:

  • Javascript
  • .Net Core com SignalR e msgpack5

Meu código é baseado nesta postagem . A única diferença é que eu estou usando ContractlessStandardResolver.Instance.

TGH
fonte
A representação de ponto flutuante em C # também não é exata para todos os valores. Dê uma olhada nos dados serializados. Como você o analisa em c #?
JeffRSon 27/03
Que tipo você usa em c #? Sabe-se que o dobro tem esse problema.
Poul Bak
Eu uso a serilização / desserialização do pacote de mensagens que acompanha o signalr e sua integração com o pacote de mensagens.
TGH 27/03
Os valores do ponto flutuante nunca são precisos. Se você precisar de valores precisos, use cadeias (problema de formatação) ou números inteiros (por exemplo, multiplicando por 1000).
atmin 31/03
Você pode verificar a mensagem desserializada? O texto que você recebeu de js, antes de c # se converter em um objeto.
Jonny Piazzi

Respostas:

9

ATUALIZAR

Isso foi corrigido na próxima versão (5.0.0-preview4) .

Resposta original

Eu testei floate double, e , curiosamente, neste caso em particular, só doubletive o problema, enquanto floatparece estar funcionando (ou seja, 0,005 é lido no servidor).

A inspeção nos bytes da mensagem sugeriu que 0,005 é enviado como um tipo de Float32Doubleponto flutuante de precisão única de 4 bytes / 32 bits IEEE 754, apesar de Numberser um ponto flutuante de 64 bits.

Execute o seguinte código no console confirmou o acima:

msgpack5().encode(Number(0.005))

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

O mspack5 fornece uma opção para forçar o ponto flutuante de 64 bits:

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

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

No entanto, a forceFloat64opção não é usada pelo signalr-protocol-msgpack .

Embora isso explique por que floatfunciona no lado do servidor, mas não há realmente uma correção para isso a partir de agora . Vamos esperar o que a Microsoft diz .

Soluções possíveis

  • Hack opções de msgpack5? Bifurque e compile seu próprio msgpack5 com o forceFloat64padrão true? Eu não sei.
  • Mudar para o floatlado do servidor
  • Use stringnos dois lados
  • Alterne para o decimallado do servidor e escreva personalizado IFormatterProvider. decimalnão é do tipo primitivo e IFormatterProvider<decimal>é chamado para propriedades de tipo complexas
  • Forneça um método para recuperar o doublevalor da propriedade e faça o truque double-> float-> decimal->double
  • Outras soluções irrealistas em que você poderia pensar

TL; DR

O problema com o cliente JS enviando um número de ponto flutuante único para o back-end em C # causa um problema conhecido de ponto flutuante:

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

Para usos diretos de doublemétodos, o problema pode ser resolvido por um costume 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 use o resolvedor:

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

O resolvedor não é perfeito, pois a conversão para decimalentão doubleatrasa o processo e pode ser perigoso .

Contudo

Conforme o OP apontado nos comentários, isso não pode resolver o problema se estiver usando tipos complexos com doublepropriedades retornadas.

Uma investigação mais aprofundada revelou a causa do problema no 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;
    }
  }
}

O decodificador acima é usado quando é necessário converter um único floatnúmero para double:

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

v2

Esse problema existe nas versões v2 do MessagePack-CSharp. Eu arquivei um problema no github , embora o problema não seja corrigido .

weichch
fonte
Achados interessantes. Um desafio aqui é que o problema se aplica a qualquer número de propriedades duplas em um objeto complexo, por isso será complicado direcionar as duplas diretamente, eu acho.
TGH 30/03
@TGH Sim, você está certo. Eu acredito que é um bug no MessagePack-CSharp. Veja meu atualizado para detalhes. Por enquanto, você pode precisar usar floatcomo solução alternativa. Não sei se eles corrigiram isso na v2. Vou dar uma olhada assim que tiver algum tempo. No entanto, o problema é que a v2 ainda não é compatível com o SignalR. Somente versões de visualização (5.0.0.0- *) do SignalR podem usar a v2.
weichch 31/03
Isso também não está funcionando na v2. Eu levantei um bug com o MessagePack-CSharp.
weichch 31/03
@TGH Infelizmente, não há nenhuma correção no servidor, de acordo com a discussão na questão do github. A melhor solução seria fazer com que o lado do cliente envie 64 bits em vez de 32 bits. Percebi que há uma opção para forçar isso a acontecer, mas a Microsoft não expõe isso (pelo que entendi). Resposta atualizada apenas com algumas soluções desagradáveis, se você quiser dar uma olhada. E boa sorte nesta questão.
weichch
Isso soa como uma pista interessante. Vou dar uma olhada nisso. Obrigado por sua ajuda com isso!
TGH 01/04
14

Verifique o valor exato que você está enviando para obter uma precisão maior. Os idiomas normalmente limitam a precisão na impressão para torná-la melhor.

var n = Number(0.005);
console.log(n);
0.005
console.log(n.toPrecision(100));
0.00500000000000000010408340855860842566471546888351440429687500000000...
Pete
fonte
Sim, você está certo sobre isso.
TGH 27/03