Reinterpreting the bits of a 64-bit integer as if they were a double-precision floating point number (and vice versa)


Today's Little Program takes a 64-bit integer and reinterprets its physical representation as a double-precision floating point number.

using System;

class Program
{
 static double ReinterpretAsDouble(long longValue)
 {
  return BitConverter.ToDouble(BitConverter.GetBytes(longValue), 0);
 }

 static long ReinterpretAsLong(double doubleValue)
 {
  return BitConverter.ToInt64(BitConverter.GetBytes(doubleValue), 0);
 }

 static void Main()
 {
  Console.WriteLine(ReinterpretAsDouble(0x4000000000000000));
  Console.WriteLine("{0:X}", ReinterpretAsLong(2.0));
 }
}

Our first attempt uses the Bit­Converter class to convert the 64-bit integer to an array of bytes, and then parses a double-precision floating point number from that byte array.

Maybe you're not happy that this creates a short-lived byte[] array that will need to be GC'd. So here's another version that is a little sneakier.

using System;
using System.Runtime.InteropServices;

class Program
{
 [StructLayout(LayoutKind.Explicit)]
 struct LongAndDouble
 {
  [FieldOffset(0)] public long longValue;
  [FieldOffset(0)] public double doubleValue;
 }

 static double ReinterpretAsDouble(long longValue)
 {
  LongAndDouble both;
  both.doubleValue = 0.0;
  both.longValue = longValue;
  return both.doubleValue;
 }

 static long ReinterpretAsLong(double doubleValue)
 {
  LongAndDouble both;
  both.longValue = 0;
  both.doubleValue = doubleValue;
  return both.longValue;
 }
 ...
}

This version creates a structure with an unusual layout: The two members occupy the same physical storage. The conversion is done by storing the 64-bit integer into that storage location, then reading the double-precision floating point value out.

There's a third method that involves writing the 64-bit integer to a memory stream via Binary­Writer then reading it back with Binary­Reader, but this is clearly inferior to the Bit­Converter so I didn't bother writing it up.

Update: Damien points out that this functionality already exists in the BCL: Bit­Converter.Double­To­Int64­Bits and Bit­Converter.Int64­Bits­To­Double. But there doesn't appear to be a Bit­Converter.Float­To­Int32­Bits method, so the techniques discussed above are not completely useless.

Exercise: Why did I have to initialize the doubleValue before writing to longValue, and vice versa? What are the implications of the answer to the above question? (Yes, I could have written LongAndDouble both = new LongAndDouble();, which automatically zero-initializes everything, but then I wouldn't have had an interesting exercise!)

Comments (22)
  1. anonymouscommenter says:

    I may be being dense here but how are these different from BitConverter.DoubleToInt64Bits and BitConverter.Int64BitsToDouble?

    [I'm the one who's dense. I totally missed those methods. -Raymond]
  2. anonymouscommenter says:

    Couldn't you also just write "unsafe" code and use pointers to tell the JIT to reinterpret the long as double or double as long? The implementation of the BitConverter itself does this too, after all.

    referencesource.microsoft.com

  3. SimonRev says:

    Exercise:  The C# compiler will generate errors when you try to read an uninitialized field.  I highly doubt someone would have added code to see that it shared storage with an initialized field.  I must admit I would never have thought to emulate a C union like that.

  4. anonymouscommenter says:

    @SimonRev: But if the compiler couldn't reason about shared storage, wouldn't it just optimise either function to simply return 0?

    My first guess was that you couldn't be sure that "long" and "double" both take up the same amounts of space, so initialising the other first ensures that you never actually read from uninitialised memory no matter the architecture.

    ...and then I remembered that .NET is a fixed (virtual) architecture, and (unlike C) C#'s "long" and "double" will not vary across whatever underlying processor the code is run on. So that's not it.

  5. anonymouscommenter says:

    Exercise: the compiler performs a definite assignment check when returning a structure from a function. Like SimonRev said, there's no code in the compiler to look for the StructLayout attribute, so it would be uninitialized otherwise.

  6. barrkel says:

    FieldOffset alters metadata generated for the CLI (non-optional metadata, the metadata is not just a hint, see II.10.7 ECMA 335 2012). C#'s definite assignment rules don't operate at the same abstraction level; the C# compiler just sees a value type allocated on the stack, and fields of value types allocated on the stack need to be initialized before use.

  7. anonymouscommenter says:

    @Damien: Those two functions compute the same result, but are much better (faster, and don't create short-lived arrays for the GC to clean up).

  8. anonymouscommenter says:

    If you change the IL to remove the initialization and recompile it seems to work fine.

  9. anonymouscommenter says:

    @Zarat

    You could, but then your assembly would require full trust. Using the core libraries or Raymond's method are preferable.

    @Karellen

    The CLR initializes all memory allocations to zero. It is C#'s specification that requires a variable to be explicitly initialized before being accessed.

    @Wear

    Note that the initialization error comes from the C# compiler not the CLR. There are several things you can do in IL that individual languages which compile to IL may not support.

  10. anonymouscommenter says:

    @Ken in NH It's not really a question of support. The C# compiler is doing what the C# compiler was designed to do which is try and stop people from falling down wells.

  11. anonymouscommenter says:

    I was wondering whether the behavior of the field-aliased explicit struct layout was guaranteed to be the same as the BitConverter methods - whether the byte ordering within the explicit struct layout is guaranteed, for example. But of course, it shouldn't matter: assuming the double and long are both stored with the same endianness.

    So I was surprised to find that BitConverter's behavior is defined, according to the MSDN docs, to be dependent on whether the system architecture is little- or big-endian: "The order of bits in the integer returned by the DoubleToInt64Bits method depends on whether the computer architecture is little-endian or big-endian". That seems, now I think about it, slightly unlikely...

    It's also not helpful that the MSDN docs for Int64BitsToDouble is vague, bordering on wrong: "returns a double-precision floating point number whose value is equivalent to value". That rather suggests it returns the same thing as (double) value.

  12. anonymouscommenter says:

    My response upon reading the title: "Duh, use a union."  My response on reading the code "Oh, it's C#.  Carry on then."

    I once had to do a similar thing with explicit struct layouts.  I needed to a set the TCP keep-alive parameters on a per-connection basis from a C# application.  Ordinarily, one would do this with WSAIoctl(SIO_KEEPALIVE_VALS) msdn.microsoft.com/.../dd877220%28v=vs.85%29.aspx ; in C#, the equivalent is Socket.IOControl(), but C# doesn't ever define the required tcp_keepalive structure, and Socket.IOControl() takes a byte array for the IOCTL data.

    I could have just frobulated a byte array out of thin air (or dropped into unsafe code), but I decided that the better solution was to declare a structure with LayoutKind.Explicit, put the three integer fields in the right place, and then use Marshal.StructureToPtr and friends to marshal it into a byte array.  That way, I could create an instance of my structure, set its fields to the values I wanted, marshal it, and issue the IOCTL.  No P/Invoke, no unsafe code.

  13. anonymouscommenter says:

    I wish I had known this when I was writing my DLL loader for Windows RT.  The exploit I found to load unsigned desktop-mode code on Windows RT only let me execute dynamically-compiled C# source code without the "unsafe" keyword, and I wanted to run an unsigned native ARM code DLL.  Since ci.dll would reject NtCreateSection on the unsigned DLL, I had to write C# to manually map the DLL into memory using VirtualAlloc via P/Invoke.

    I'm inexperienced with C#, so my C# code to load the DLL into memory and execute it is a crazy mess of P/Invoke and marshaling.  It probably would've been so much easier if I'd known about this trick to make "unions".

  14. anonymouscommenter says:

    @Adam Rosenfield: Your reaction to the title was "duh use undefined behavior"? Because the union trick is exactly that.. undefined. Probably very well spotted undefined behavior by the major compilers (although with all those optimizations going on, I wouldn't count on it) but still undefined.

    The right way is to use memcpy and let the compiler optimize the generated code (they do)

  15. Medinoc says:

    This sounds unsafe. Is there a Code Access Security requirement for using StructLayout(Explicit), or is such overlap limited to value types free of reference types?

  16. anonymouscommenter says:

    @Voo: The union trick is technically undefined; however, it is an extremely common idiom and is well-supported by all major compilers. As a practical matter, reading and writing to any member of a union, in any order, is acceptable practice.

    (Source: cellperformance.beyond3d.com/.../understanding-strict-aliasing.html )

  17. anonymouscommenter says:

    @Medonic: Only value types are permitted to overlap.

  18. anonymouscommenter says:

    @snarfy I just tried having a struct with two class fields overlapping and it seems perfectly fine with it.

    Assign a value to one field and it shows up in the other. It seems to act as though the two objects where smashed on top of each other. One class has an int field that is set to 2, the other has a float field that should be set to 1. After setting the field in the struct to the one class and getting the value of the other it's float field reports a value of 2.802597E-45.

  19. cheong00 says:

    It's interesting that when use "LongAndDouble both = new LongAndDouble();" in the methods will case the result will be "0", but if you replace the content with (for ReinterpretAsLong()) "return new LongAndDouble() { doubleValue = doubleValue }.longValue;" the result is the corrent one.

    The test is performed on .NET v3.5.

  20. anonymouscommenter says:

    @Wear: So, falling down wells is big problem for programmers where you live? ;-)

    [That is actually one of the major goals of software design. See: "The pit of success". -Raymond]
  21. Myria says:

    @Voo: Visual C++ is actually the only compiler that I've seen that *doesn't* recognize std::memcpy being used just to do type punning.

  22. anonymouscommenter says:

    Be careful about getting too smart with the union trick.

    I had a byte array equality function that was being called many, many times and became a candidate for optimization during profiling. The obvious solution was to use P/Invoke to call memcmp, but for some reason on that day it eluded me. The trick I came up was to union the byte array with a long array and by comparing the longs I would cut down the execution time (in theory) by a factor of 8.

    "If" statements began to execute the wrong branch: stackoverflow.com/.../if-statement-true-block-executed-when-condition-is-false

Comments are closed.

Skip to main content