JavaScript에서 C #으로 숫자 정밀도 손실


16

MessagePack과 함께 SignalR을 사용하여 JavaScript와 C # 간의 값을 직렬화 및 역 직렬화하는 경우 수신 측에서 C #에 약간의 정밀 손실이 나타납니다.

예를 들어 JavaScript에서 C #으로 0.005 값을 보내고 있습니다. deserialized 값이 C # 측에 나타나면 value 0.004999999888241291가 가깝습니다. 그러나 정확히 0.005는 아닙니다. JavaScript 측의 값 Number은 C # 측에서 사용하고 double있습니다.

JavaScript는 부동 소수점 숫자를 정확하게 표현할 수 없으므로 이와 같은 결과를 초래할 수 있음을 읽었습니다 0.1 + 0.2 == 0.30000000000000004. 내가보고있는 문제 가이 JavaScript 기능과 관련이 있다고 생각합니다.

흥미로운 부분은 같은 문제가 다른 방향으로 가고 있지 않다는 것입니다. C #에서 JavaScript로 0.005를 보내면 JavaScript에서 값이 0.005가됩니다.

편집 : C #의 값이 JS 디버거 창에서 짧아졌습니다. @Pete가 언급했듯이 정확히 0.5가 아닌 것으로 확장됩니다 (0.005000000000000000104083408558). 이것은 적어도 불일치가 양쪽에서 발생한다는 것을 의미합니다.

JSON 직렬화는 문자열을 통과한다고 가정하기 때문에 동일한 문제가 없습니다. 수신 환경이 제어 wrt에서 값을 기본 숫자 유형으로 구문 분석하는 것을 허용합니다.

이진 직렬화를 사용하여 양쪽에 일치하는 값을 갖는 방법이 있는지 궁금합니다.

그렇지 않으면 JavaScript와 C #간에 100 % 정확한 이진 변환을 수행 할 수있는 방법이 없다는 의미입니까?

사용 된 기술 :

  • 자바 스크립트
  • SignalR 및 msgpack5가 포함 된 .Net Core

내 코드는 이 게시물을 기반으로 합니다 . 유일한 차이점은 내가 사용하고 있다는 것 ContractlessStandardResolver.Instance입니다.


C #의 부동 소수점 표현이 모든 값에 대해 정확하지는 않습니다. 직렬화 된 데이터를 살펴보십시오. C #에서 어떻게 파싱합니까?
JeffRSon

C #에서 어떤 유형을 사용합니까? Double은 이러한 문제가있는 것으로 알려져 있습니다.
Poul Bak

신호기와 함께 제공되는 기본 제공 메시지 팩 serilization / deserialization을 사용하며 메시지 팩 통합입니다.
TGH

부동 소수점 값은 정확하지 않습니다. 정확한 값이 필요한 경우 문자열 (포맷 문제) 또는 정수 (예 : 1000을 곱한 값)를 사용하십시오.
atmin

역 직렬화 된 메시지를 확인할 수 있습니까? c #이 객체에서 변환되기 전에 js에서 얻은 텍스트.
Jonny Piazzi

답변:


9

최신 정보

이것은 다음 릴리스 (5.0.0-preview4)에서 수정 되었습니다 .

원래 답변

나는 이 특별한 경우에 테스트 float하고 double흥미롭게 만 double문제가있는 반면 float작동 하는 것처럼 보입니다 (즉, 서버에서 0.005를 읽습니다).

메시지 바이트를 조사한 결과, 0.005 가 64 비트 부동 소수점 Float32Double임에도 불구하고 4 바이트 / 32 비트 IEEE 754 단 정밀도 부동 소수점 수인 유형으로 전송됨을 제안했습니다 Number.

콘솔에서 다음 코드를 실행하여 위를 확인하십시오.

msgpack5().encode(Number(0.005))

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

mspack5 는 64 비트 부동 소수점을 강제하는 옵션을 제공합니다.

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

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

그러나 signalr-protocol-msgpack은이forceFloat64 옵션을 사용하지 않습니다 .

그것은 왜 float서버 측에서 작동 하는지 설명 하지만, 현재로서는 그에 대한 해결책이 없습니다 . 마이크로 소프트의 말을 기다리 자 .

가능한 해결 방법

  • msgpack5 옵션을 해킹 하시겠습니까? 포크로 자신의 msgpack5를 forceFloat64기본값으로 true로 컴파일하십시오 ?? 모르겠어요
  • float서버 측으로 전환
  • 사용하여 string양쪽에
  • decimal서버 측으로 전환하고 custom을 작성하십시오 IFormatterProvider. decimal기본 유형이 아니며 IFormatterProvider<decimal>복합 유형 특성에 대해 호출됩니다.
  • double속성 값 을 검색 하고 double-> float-> decimal-> double트릭을 수행하는 방법을 제공하십시오.
  • 생각할 수있는 다른 비현실적인 솔루션

TL; DR

JS 클라이언트가 단일 부동 소수점 숫자를 C # 백엔드로 보내는 문제로 인해 알려진 부동 소수점 문제가 발생합니다.

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

doublein 메소드 를 직접 사용 하는 경우 사용자 지정으로 문제를 해결할 수 있습니다 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;
    }
}

그리고 리졸버를 사용하십시오 :

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

decimal다음 으로 캐스팅 double하면 프로세스 속도가 느려지고 위험 할 수 있으므로 리졸버는 완벽하지 않습니다 .

하나

주석에서 지적한 OP에 따라 속성 을 반환 하는 복잡한 유형을 사용하는 경우 문제를 해결할 수 없습니다double .

추가 조사 결과 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;
    }
  }
}

위의 디코더는 단일 float숫자를 double다음 으로 변환해야 할 때 사용됩니다 .

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

v2

이 문제는 v2 버전의 MessagePack-CSharp에 존재합니다. 내가 제기 한 GitHub의에 문제를 , 문제가 해결 될 것 아니지만 .


흥미로운 발견. 여기서 한 가지 문제는 복잡한 개체의 여러 이중 속성에 문제가 적용되므로 이중을 직접 대상으로 지정하는 것이 까다로울 것입니다.
TGH

@TGH 그래, 맞아. MessagePack-CSharp의 버그라고 생각합니다. 자세한 내용은 내 업데이트를 참조하십시오. 지금 float은 해결 방법 으로 사용해야 할 수도 있습니다 . v2에서 수정했는지 모르겠습니다. 시간이 있으면 한번 살펴 보겠습니다. 그러나 문제는 v2가 아직 SignalR과 호환되지 않는다는 것입니다. SignalR의 미리보기 버전 (5.0.0.0- *) 만 v2를 사용할 수 있습니다.
weichch

이것은 v2에서도 작동하지 않습니다. MessagePack-CSharp에서 버그가 발생했습니다.
weichch

@TGH 불행히도 github 문제에 대한 토론에 따라 서버 측에는 수정 사항이 없습니다. 가장 좋은 해결책은 클라이언트 쪽이 32 비트가 아닌 64 비트를 보내도록하는 것입니다. 나는 그것을 강제로 할 수있는 옵션이 있음을 알았지 만 Microsoft는 (내 이해에서) 그것을 공개하지 않습니다. 살펴보고 싶다면 불쾌한 해결 방법으로 답변을 업데이트했습니다. 이 문제에 행운을 빕니다.
weichch

흥미로운 리드 인 것 같습니다. 나는 그것을 볼 것입니다. 도와 주셔서 감사합니다!
TGH

14

더 정확한 값을 보내려면 정확한 값을 확인하십시오. 언어는 일반적으로 인쇄물의 정밀도를 제한하여보기에 좋습니다.

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

그렇습니다.
TGH
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.