3
votes

I make a conversion "bytes[4] -> float number -> bytes[4]" without any arithmetics. In bytes I have a single precision number in IEEE-754 format (4 bytes per number, little endian order as in a machine). I encounter a issue, when bytes represents a NaN value converted not verbatim. For example:

{ 0x1B, 0xC4, 0xAB, 0x7F } -> NaN -> { 0x1B, 0xC4, 0xEB, 0x7F }

Code for reproduction:

using System;
using System.Linq;

namespace StrangeFloat
{
    class Program
    {
        private static void PrintBytes(byte[] array)
        {
            foreach (byte b in array)
            {
                Console.Write("{0:X2}", b);
            }
            Console.WriteLine();
        }

        static void Main(string[] args)
        {
            byte[] strangeFloat = { 0x1B, 0xC4, 0xAB, 0x7F };
            float[] array = new float[1];
            Buffer.BlockCopy(strangeFloat, 0, array, 0, 4);
            byte[] bitConverterResult = BitConverter.GetBytes(array[0]);

            PrintBytes(strangeFloat);
            PrintBytes(bitConverterResult);
            bool isEqual = strangeFloat.SequenceEqual(bitConverterResult);
            Console.WriteLine("IsEqual: {0}", isEqual);
        }
    }
}

Result ( https://ideone.com/p5fsrE ):

1BC4AB7F
1BC4EB7F
IsEqual: False

This behaviour depends from platform and configuration: this code convert a number without errors on x64 in all configurations or in x86/Debug. On x86/Release an error exists.

Also, if I change

byte[] bitConverterResult = BitConverter.GetBytes(array[0]);

to

float f = array[0];
byte[] bitConverterResult = BitConverter.GetBytes(f);

then it erroneus also on x86/Debug.

I do research the problem and found that compiler generate x86 code that use a FPU registers (!) to a hold a float value (FLD/FST instructions). But FPU set a high bit of mantissa to 1 instead of 0, so it modify value although logic was is just pass a value without change. On x64 platform a xmm0 register used (SSE) and it works fine.

[Question]

What is this: it is a somewhere documented undefined behavior for a NaN values or a JIT/optimization bug?

Why compiler use a FPU and SSE when no arithmetic operations was made?

Update 1

Debug configuration - pass value via stack without side effects - correct result:

    byte[] bitConverterResult = BitConverter.GetBytes(array[0]);
02232E45  mov         eax,dword ptr [ebp-44h]  
02232E48  cmp         dword ptr [eax+4],0  
02232E4C  ja          02232E53  
02232E4E  call        71EAC65A  
02232E53  push        dword ptr [eax+8]   // eax+8 points to "1b c4 ab 7f" CORRECT!
02232E56  call        7136D8E4  
02232E5B  mov         dword ptr [ebp-5Ch],eax // eax points to managed
// array data "fc 35 d7 70 04 00 00 00 __1b c4 ab 7f__" and this is correct
02232E5E  mov         eax,dword ptr [ebp-5Ch]  
02232E61  mov         dword ptr [ebp-48h],eax 

Release configuration - optimizer or a JIT does a strange pass via FPU registers and breaks a data - incorrect

    byte[] bitConverterResult = BitConverter.GetBytes(array[0]);
00B12DE8  cmp         dword ptr [edi+4],0  
00B12DEC  jbe         00B12E3B  
00B12DEE  fld         dword ptr [edi+8]     // edi+8 points to "1b c4 ab 7f"
00B12DF1  fstp        dword ptr [ebp-10h]   // ebp-10h points to "1b c4 eb 7f" (FAIL)
00B12DF4  mov         ecx,dword ptr [ebp-10h]  
00B12DF7  call        70C75810  
00B12DFC  mov         edi,eax  
00B12DFE  mov         ecx,esi  
00B12E00  call        dword ptr ds:[4A70860h] 
1
There are multiple values that are valid for NaN in the IEEE spec. - leppie
Do you get same result with Debug and Release? I believe debug is using software to simulate FPU while release use FPU in computer. How old is the PC? I believe therre are known issues with some UP floating point unit. - jdweng
Intel processor manual: "If either or both of the source operands are NaNs and floating-point invalid-operation exception is masked, the result is as shown in Table 4-7. When an SNaN is converted to a QNaN, the conversion is handled by setting the most-significant fraction bit of the SNaN to 1. Also, when one of the source operands is an SNaN, the floatingpoint invalid-operation exception flag it set. Note that for some combinations of source operands, the result is different for x87 FPU operations and for SSE/SSE2/SSE3/SSE4.1 operations. Intel AVX follows the same behavior as SSE/SSE2..." - Hans Passant
@jdweng I get different results with Debug and Release, please see post update: in debug mode, data passed via stack and this is ok, but in release mode data pumped via FPU - why optimizer/JIT is doing this? (CPU Core2 Quad Q9550, this is not a hardware issue) - Dmitry Razumikhin
The x86 jitter uses the FPU to handle floating point values. This is not a bug. Your assumption that those byte values are a proper argument to a method that takes a float argument is just wrong. The only use for a signaling NaN is to generate an exception. The .NET Framework doesn't. And no floating point operation performed by .NET code could ever generate those byte values. You need to investigate the source of those byte values, it has a bug. - Hans Passant

1 Answers

1
votes

I just translate @HansPassant comment as an answer.

"The x86 jitter uses the FPU to handle floating point values. This is not a bug. Your assumption that those byte values are a proper argument to a method that takes a float argument is just wrong."

In other words, this is just a GIGO case (Garbage In, Garbage Out).