JavaScript ke C # Numeric Precision Loss

16

Ketika serialisasi dan deserializing nilai antara JavaScript dan C # menggunakan SignalR dengan MessagePack saya melihat sedikit kehilangan presisi dalam C # di sisi penerima.

Sebagai contoh, saya mengirim nilai 0,005 dari JavaScript ke C #. Ketika nilai deserialized muncul di sisi C # saya mendapatkan nilai 0.004999999888241291, yang dekat, tetapi tidak tepat 0,005. Nilai di sisi JavaScript adalah Numberdan di sisi C # yang saya gunakan double.

Saya telah membaca bahwa JavaScript tidak dapat mewakili angka floating point persis yang dapat menyebabkan hasil seperti 0.1 + 0.2 == 0.30000000000000004. Saya menduga masalah yang saya lihat terkait dengan fitur JavaScript ini.

Bagian yang menarik adalah bahwa saya tidak melihat masalah yang sama terjadi sebaliknya. Mengirim 0,005 dari C # ke JavaScript menghasilkan nilai 0,005 dalam JavaScript.

Sunting : Nilai dari C # disingkat di jendela debugger JS. Seperti @Pete sebutkan itu memperluas ke sesuatu yang tidak tepat 0,5 (0,0050000000000000000004040404058) Ini berarti perbedaan setidaknya terjadi di kedua sisi.

Serialisasi JSON tidak memiliki masalah yang sama karena saya mengasumsikannya berjalan melalui string yang membuat lingkungan penerima dalam kontrol wrt mengurai nilai ke dalam tipe numerik aslinya.

Saya bertanya-tanya apakah ada cara menggunakan serialisasi biner untuk memiliki nilai yang cocok di kedua sisi.

Jika tidak, apakah ini berarti bahwa tidak ada cara untuk memiliki konversi biner 100% akurat antara JavaScript dan C #?

Teknologi yang digunakan:

  • JavaScript
  • .Net Core dengan SignalR dan msgpack5

Kode saya didasarkan pada posting ini . Satu-satunya perbedaan adalah bahwa saya menggunakan ContractlessStandardResolver.Instance.

TGH
sumber
Representasi floating point dalam C # tidak tepat untuk setiap nilai juga. Lihatlah data serial. Bagaimana Anda menguraikannya dalam C #?
JeffRSon
Jenis apa yang Anda gunakan dalam C #? Double diketahui memiliki masalah seperti itu.
Poul Bak
Saya menggunakan built-in serilisasi paket pesan / deserialization yang datang dengan signalr dan itu integrasi paket pesan.
TGH
Nilai floating point tidak pernah tepat. Jika Anda membutuhkan nilai yang tepat, gunakan string (masalah pemformatan) atau bilangan bulat (mis. Dengan mengalikannya dengan 1000).
atmin
Bisakah Anda memeriksa pesan deserialized? Teks yang Anda dapatkan dari js, sebelum c # dikonversi dalam suatu objek.
Jonny Piazzi

Jawaban:

9

MEMPERBARUI

Ini telah diperbaiki pada rilis berikutnya (5.0.0-preview4) .

Jawaban Asli

Saya menguji floatdan double, dan yang menarik dalam kasus khusus ini, hanya doublepunya masalah, sedangkan floattampaknya berfungsi (yaitu 0,005 dibaca di server).

Memeriksa byte pesan menyarankan bahwa 0,005 dikirim sebagai tipe Float32Doubleyang merupakan nomor 4-byte / 32-bit floating point presisi tunggal IEEE 754 meskipun Number64 bit floating point.

Jalankan kode berikut di konsol yang mengkonfirmasi hal di atas:

msgpack5().encode(Number(0.005))

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

mspack5 memang menyediakan opsi untuk memaksa floating point 64 bit:

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

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

Namun, forceFloat64opsi ini tidak digunakan oleh signalr-protokol-msgpack .

Meskipun itu menjelaskan mengapa floatbekerja di sisi server, tetapi sebenarnya tidak ada perbaikan untuk itu sampai sekarang . Mari kita tunggu apa yang dikatakan Microsoft .

Kemungkinan solusi

  • Opsi hack msgpack5? Fork dan kompilasi msgpack5 Anda sendiri dengan forceFloat64default to true ?? Saya tidak tahu
  • Beralih ke floatdi sisi server
  • Gunakan stringdi kedua sisi
  • Beralihlah ke decimalsisi server dan tulis kustom IFormatterProvider. decimalbukan tipe primitif, dan IFormatterProvider<decimal>dipanggil untuk properti tipe kompleks
  • Berikan metode untuk mengambil doublenilai properti dan lakukan trik double-> float-> decimal->double
  • Solusi tidak realistis lainnya yang dapat Anda pikirkan

TL; DR

Masalah dengan klien JS yang mengirim nomor floating point tunggal ke C # backend menyebabkan masalah floating point yang diketahui:

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

Untuk penggunaan langsung doublemetode, masalah dapat diselesaikan dengan kebiasaan 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;
    }
}

Dan gunakan resolver:

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

Penyelesai tidak sempurna, karena casting decimaluntuk doublememperlambat proses dan itu bisa berbahaya .

Namun

Seperti OP yang ditunjukkan dalam komentar, ini tidak dapat menyelesaikan masalah jika menggunakan tipe kompleks yang memiliki doubleproperti yang dikembalikan.

Investigasi lebih lanjut mengungkapkan penyebab masalah di 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;
    }
  }
}

Dekoder di atas digunakan ketika perlu mengkonversi satu floatnomor ke double:

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

v2

Masalah ini ada di versi v2 MessagePack-CSharp. Saya telah mengajukan masalah pada github , meskipun masalah ini tidak akan diperbaiki .

Weichch
sumber
Temuan menarik. Satu tantangan di sini adalah bahwa masalah ini berlaku untuk sejumlah properti ganda pada objek yang kompleks, jadi akan sulit untuk menargetkan ganda secara langsung, saya pikir.
TGH
@TGH Ya, kamu benar. Saya percaya ini adalah bug di MessagePack-CSharp. Lihat saya diperbarui untuk detail. Untuk saat ini, Anda mungkin perlu menggunakan floatsolusi. Saya tidak tahu apakah mereka sudah memperbaikinya di v2. Saya akan melihat sekali saya punya waktu. Namun, masalahnya v2 belum kompatibel dengan SignalR. Hanya versi pratinjau (5.0.0.0- *) dari SignalR yang dapat menggunakan v2.
weichch
Ini juga tidak berfungsi di v2. Saya telah meningkatkan bug dengan MessagePack-CSharp.
weichch
@ THG Sayangnya tidak ada perbaikan di sisi server sesuai diskusi dalam masalah github. Perbaikan terbaik adalah membuat pihak klien mengirim 64bits daripada 32bits. Saya perhatikan ada opsi untuk memaksa itu terjadi, tetapi Microsoft tidak mengungkapkan itu (dari pemahaman saya). Baru saja memperbarui jawaban dengan beberapa solusi buruk jika Anda ingin melihatnya. Dan semoga berhasil dalam masalah ini.
weichch
Kedengarannya seperti petunjuk yang menarik. Saya akan lihat itu. Terima kasih atas bantuan Anda dengan ini!
TGH
14

Silakan periksa nilai tepat yang Anda kirim ke presisi yang lebih besar. Bahasa biasanya membatasi presisi pada cetakan untuk membuatnya terlihat lebih baik.

var n = Number(0.005);
console.log(n);
0.005
console.log(n.toPrecision(100));
0.00500000000000000010408340855860842566471546888351440429687500000000...
Pete
sumber
Ya, Anda benar tentang itu.
TGH