6

Is the IEEE 754 floating point format well defined across platforms? In terms of both bit format and endianness?

I am willing to add the following to my code (for an initial version):

static_assert(std::numeric_limits<float>::is_iec559, "Only support IEC 559 (IEEE 754) float");
static_assert(sizeof(float) * CHAR_BIT == 32, "Only support float => Single Precision IEC 559 (IEEE 754)");

static_assert(std::numeric_limits<double>::is_iec559, "Only support IEC 559 (IEEE 754) double");
static_assert(sizeof(float) * CHAR_BIT == 64, "Only support double => Double Precision IEC 559 (IEEE 754)");

static_assert(std::numeric_limits<long double>::is_iec559, "Only support IEC 559 (IEEE 754) long double");
static_assert(sizeof(float) * CHAR_BIT == 128, "Only support long double  => Exteneded Precision IEC 559 (IEEE 754)");
//  More asserts if required.
//  I noticed my current system has a sizeof(long double) => 128
//  But numeric_limits<long double>::digits  => 63
//  So we are not storing quad precision floats only extended.

If I write my float/double/long double in binary format can these be transported between systems without further interpretation. ie...

void write(std::ostream& stream, double value)
{
     stream.write(reinterpret_cast<char const*>(&value), 8);
}

....

double read(std::istream& stream)
{
     double   value;
     stream.read(reinterpret_cast<char*>(&value), 8);
     return value;
}

Or do I need to break the double up into integer components for transport (as suggested by this answer):

The difference here is I am willing to limit my supported representation to IEEE-754 will this basically solve my binary storage of floating point values or do I need to take further steps?

Note: For non conforming platforms (when I find them) I am willing to special case the code so that they read/write IEEE-754 into local representation. But I want to know if the bit/endian is well enough defined cross platform to support storage/transport.

Community
  • 1
  • 1
Martin York
  • 257,169
  • 86
  • 333
  • 562
  • Endianness is not specified. So you'll want to convert everything to either big- or little-endian. – tmyklebu Feb 20 '15 at 14:12
  • @tmyklebu: Do you have a reference I can read that says that? Everybody else I talk to says it is (though they have not shown me the standard either). – Martin York Feb 20 '15 at 16:58
  • I'm confused. You want a reference that says that another reference doesn't say something? – tmyklebu Feb 20 '15 at 17:34
  • @tmyklebu: Sorry that came out wrong (I also would err on the side of "unless explicitly specified we can not assume", so currently I am of the mind I must take it into account). But I want to understand why you say that (as I keep hearing that endianness is not a factor) which is why I am asking the question. I want to get some explicit references to how I can expect to see this in memory. As you see currently the only answer is implying it is not an issue (and has no down votes, though 1 up vote not a ringing endorsement either). – Martin York Feb 20 '15 at 18:30
  • Well, I'm saying this because printing out the bytes of the `double` `42.0` on a SPARC gives you `40 45 00 00 00 00 00 00` while doing it on an x86_64 machine gives you `00 00 00 00 00 00 45 40`. These are different because SPARC is big-endian and x86_64 is little-endian. – tmyklebu Feb 20 '15 at 19:28
  • @tmyklebu: That's a very convincing argument. Can you provide an answer below and provide the code you use to generate those value (making sure you print out the `std::numeric_limits::is_iec559` and `std::numeric_limits::digits`) value. – Martin York Feb 20 '15 at 19:47
  • One usual non-conforming platform used to be DEC VAX; and on this platform, floating-point values are stored in a baroque (PDP-11 inherited) format which is defying any attempt to have endian-ness standardized! – AntoineL Mar 05 '15 at 16:45

3 Answers3

4

Bit format is well-defined, but not all machines are little-endian. The IEEE standard does not require floating-point numbers to be a certain endian, either. You can run the following program to see the byte pattern of the double 42.0:

#include <stdio.h>
#include <numeric>
#include <limits>
using namespace std;

int main() {
  double d = 42;
  printf("%i\n", std::numeric_limits<double>::is_iec559);
  for (char *c = (char *)&d; c != (char *)(&d+1); c++) {
    printf("%02hhx ", *c);
  }
  printf("\n");
}

On an old, unmaintained Sun machine using g++ 3.4.5, this prints

1
40 45 00 00 00 00 00 00

On an x86_64 machine running a much more recent g++:

1
00 00 00 00 00 00 45 40
tmyklebu
  • 13,915
  • 3
  • 28
  • 57
  • What is the value of: `std::numeric_limits::digits` – Martin York Feb 20 '15 at 20:33
  • 1
    But I am pretty sure that is conclusive proof that the endianess is not part of the standard and thus I can not use the simple technique in the question and must do some work to compensate for different architectures. – Martin York Feb 20 '15 at 20:36
  • @LokiAstari: `digits` is 53 on both platforms. Apart from it not being in the standard, the other argument for why endianness isn't standard is that *little*-endian platforms are the ones that would have gotten screwed if it were part of the standard. Also, it would be inconvenient to have the significand of a number represented in the wrong endian on any platform. – tmyklebu Feb 21 '15 at 01:38
  • The reason for little endian has something to do with the carry bit when doing integer addition. It was more efficient to layout the transistors the other way around (something that has since long been made redundant). How this affects the layout of FP numbers I am not sure since most systems pass this functionality out to a specialized unit independent of the main CPU. So i am not sure why the layout of FP numbers in memory should be affected systems in anyway (but I am not hardware engineer). As far as I can tell it is just to be consistent with integers (which has its advantages I suppose). – Martin York Feb 21 '15 at 18:39
1

First of all, you may want to change your code such that it properly checks for the type sizes...

static_assert(std::numeric_limits<float>::is_iec559, "Only support IEC 559 (IEEE 754) float");
static_assert(sizeof(float) * CHAR_BIT == 32, "Only support float => Single Precision IEC 559 (IEEE 754)");

static_assert(std::numeric_limits<double>::is_iec559, "Only support IEC 559 (IEEE 754) double");
static_assert(sizeof(double) * CHAR_BIT == 64, "Only support double => Double Precision IEC 559 (IEEE 754)");

static_assert(std::numeric_limits<long double>::is_iec559, "Only support IEC 559 (IEEE 754) long double");
static_assert(sizeof(long double) * CHAR_BIT == 128, "Only support long double  => Exteneded Precision IEC 559 (IEEE 754)");

The thing is, that IEEE-754 does not require long double to be 128 bit long. Depending on the compiler and platform, the length of such type may vary. It does however specify binary128, which may be or may be not supported by the compiler, depending on the platform and the implementation (gcc has a non-standard __float128 type for that). The standard only requires long double to be at least as precise, as double, making it usually 80 bit long (gcc) or 64 (VS).

If you limit your supported representation to IEEE-754, you should not run into any problems.

Marandil
  • 1,042
  • 1
  • 15
  • 31
1

To read and write IEEE 754 portably, use these routines. If the platform is not IEEE 754, you might lose a couple of bits, but you will still get the closest possible representation.

https://github.com/MalcolmMcLean/ieee754

Malcolm McLean
  • 6,258
  • 1
  • 17
  • 18