JavaScript do C # Utrata precyzji numerycznej

16

Podczas szeregowania i deserializacji wartości między JavaScript i C # za pomocą SignalR z MessagePack widzę trochę utraty precyzji w C # na końcu odbierającym.

Jako przykład przesyłam wartość 0,005 z JavaScript do C #. Gdy po stronie C # pojawia się deserializowana wartość, otrzymuję wartość 0.004999999888241291, która jest bliska, ale nie dokładnie 0,005. Wartość po stronie JavaScript to Numberi po stronie C # używam double.

Czytałem, że JavaScript nie może dokładnie reprezentować liczb zmiennoprzecinkowych, co może prowadzić do podobnych wyników 0.1 + 0.2 == 0.30000000000000004. Podejrzewam, że problem, który widzę, jest związany z tą funkcją JavaScript.

Interesujące jest to, że nie widzę tego samego problemu idącego w drugą stronę. Przesłanie 0,005 z C # do JavaScript skutkuje wartością 0,005 w JavaScript.

Edycja : Wartość z C # jest właśnie skrócona w oknie debugera JS. Jak wspomniano @Pete, rozwija się do czegoś, co nie jest dokładnie 0,5 (0,005000000000000000104083408558). Oznacza to, że rozbieżność zdarza się przynajmniej po obu stronach.

Serializacja JSON nie ma tego samego problemu, ponieważ zakładam, że przechodzi ona przez łańcuch znaków, który pozostawia środowisko odbiorcze w kontroli parsującej wartość do jej rodzimego typu numerycznego.

Zastanawiam się, czy istnieje sposób użycia serializacji binarnej, aby uzyskać pasujące wartości po obu stronach.

Jeśli nie, to czy to oznacza, że ​​nie ma możliwości uzyskania 100% dokładnych konwersji binarnych między JavaScript i C #?

Zastosowana technologia:

  • JavaScript
  • .Net Core z SignalR i msgpack5

Mój kod oparty jest na tym poście . Jedyna różnica polega na tym, że używam ContractlessStandardResolver.Instance.

TGH
źródło
Reprezentacja zmiennoprzecinkowa w języku C # również nie jest dokładna dla każdej wartości. Spójrz na zserializowane dane. Jak to parsować w C #?
JeffRSon
Jakiego typu używasz w C #? Wiadomo, że Double ma taki problem.
Poul Bak
Korzystam z wbudowanej serilizacji / deserializacji pakietu komunikatów, która jest dostarczana z Signalr i jego integracją z pakietem wiadomości.
TGH
Wartości zmiennoprzecinkowe nigdy nie są precyzyjne. Jeśli potrzebujesz dokładnych wartości, użyj ciągów znaków (problem z formatowaniem) lub liczb całkowitych (np. Mnożąc przez 1000).
atmin
Czy możesz sprawdzić deserializowaną wiadomość? Tekst otrzymany z js, zanim c # przekształci się w obiekt.
Jonny Piazzi

Odpowiedzi:

9

AKTUALIZACJA

Zostało to naprawione w następnej wersji (5.0.0-Preview4) .

Oryginalna odpowiedź

Testowałem floati double, co ciekawe, w tym konkretnym przypadku, doublemiałem tylko problem, podczas gdy floatwydaje się, że działa (tj. 0,005 jest odczytywany na serwerze).

Sprawdzanie bajtów wiadomości sugerowało, że 0,005 jest wysyłany jako typ, Float32Doublektóry jest 4-bajtową / 32-bitową liczbą zmiennoprzecinkową pojedynczej precyzji IEEE 754, pomimo że Numberjest liczbą zmiennoprzecinkową 64-bitową.

Uruchom następujący kod w konsoli potwierdził powyższe:

msgpack5().encode(Number(0.005))

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

mspack5 zapewnia opcję wymuszenia 64-bitowego zmiennoprzecinkowego:

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

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

Jednak forceFloat64opcja ta nie jest używana przez msgpack signalr-protokół-protokół .

Chociaż to wyjaśnia, dlaczego floatdziała po stronie serwera, ale tak naprawdę nie ma na to obecnie żadnej poprawki . Poczekajmy, co mówi Microsoft .

Możliwe obejścia

  • Włamać opcje msgpack5? Rozwidlaj i skompiluj własny msgpack5 z forceFloat64domyślną wartością true? Nie wiem
  • Przełącz na floatpo stronie serwera
  • Użyj stringpo obu stronach
  • Przełącz decimalna stronę serwera i napisz niestandardowe IFormatterProvider. decimalnie jest typem pierwotnym i IFormatterProvider<decimal>jest wywoływany dla właściwości typu złożonego
  • Podaj metodę pobierania doublewartości właściwości i wykonaj sztuczkę double-> float-> decimal->double
  • Inne nierealne rozwiązania, o których można pomyśleć

TL; DR

Problem z wysyłaniem przez klienta JS pojedynczej liczby zmiennoprzecinkowej do zaplecza C # powoduje znany problem zmiennoprzecinkowy:

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

W przypadku bezpośredniego użycia doublemetod, problem można rozwiązać niestandardowo 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;
    }
}

I użyj resolvera:

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

Resolver nie jest doskonały, jak do odlewania decimalnastępnie doublespowalnia proces w dół, a to może być niebezpieczne .

jednak

Jak wskazał PO w komentarzach, nie można rozwiązać tego problemu, jeśli używa się złożonych typów o doublezwracanych właściwościach.

Dalsze dochodzenie ujawniło przyczynę problemu w 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;
    }
  }
}

Powyższy dekoder jest używany, gdy trzeba przekonwertować pojedynczy floatnumer na double:

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

v2

Ten problem występuje w wersjach MessagePack-CSharp v2. Złożyłem problem na githubie , ale problem nie zostanie naprawiony .

weichch
źródło
Ciekawe ustalenia Jednym z wyzwań jest to, że problem dotyczy dowolnej liczby podwójnych właściwości złożonego obiektu, więc myślę, że celowanie w podwójną bezpośrednio będzie trudne.
TGH
@TGH Tak, masz rację. Uważam, że jest to błąd w MessagePack-CSharp. Zobacz moje zaktualizowane szczegóły. Na razie może być konieczne zastosowanie floatobejścia. Nie wiem, czy naprawili to w wersji 2. Przyjrzę się, kiedy będę miał trochę czasu. Problem polega jednak na tym, że wersja 2 nie jest jeszcze kompatybilna z SignalR. Tylko wersje zapoznawcze SignalR (5.0.0.0- *) mogą używać v2.
weichch
To również nie działa w v2. Podniosłem błąd w MessagePack-CSharp.
weichch
@TGH Niestety, nie ma żadnej poprawki po stronie serwera, zgodnie z dyskusją w sprawie github. Najlepszym rozwiązaniem byłoby skłonienie strony klienta do wysłania 64 bitów zamiast 32 bitów. Zauważyłem, że istnieje możliwość wymuszenia tego, ale Microsoft tego nie ujawnia (z mojego zrozumienia). Właśnie zaktualizowałem odpowiedź z kilkoma nieprzyjemnymi obejściami, jeśli chcesz rzucić okiem. Powodzenia w tej sprawie.
weichch
To brzmi jak interesujący trop. Spojrzę na to. Dzięki za pomoc w tym!
TGH
14

Sprawdź dokładną wartość, którą wysyłasz, z większą precyzją. Języki zazwyczaj ograniczają precyzję wydruku, aby wyglądał lepiej.

var n = Number(0.005);
console.log(n);
0.005
console.log(n.toPrecision(100));
0.00500000000000000010408340855860842566471546888351440429687500000000...
Pete
źródło
Tak, masz rację co do tego.
TGH