12

According to the IEEE floating point wikipage (on IEEE 754), there is a total order on double-precision floating points (i.e. on C++11 implementations having IEEE-754 floats, like gcc 4.8 on Linux / x86-64).

Of course, operator < on double is often providing a total order, but NaN are known to be exceptions (it is well known folklore that x != x is a way of testing if x, declared as double x; is a NaN).

The reason I am asking is that I want to have a.g. std::set<double> (actually, a set of JSON-like -or Python like- values) and I would like the set to have some canonical representation (my practical concern is to emit portable JSON -same data, ordered in the same order, both on Linux/x86-64 and e.g. on Linux/ARM, even in weird cases like NaN).

I cannot find any simple way to get that total order. I coded

// a totally ordering function, 
// return -1 for less-than, 0 for equal, +1 for greater
int mydoublecompare(double x, double y) { 
   if (x==y) return 0;
   else if (x<y) return -1;
   else if (x>y) return 1;
   int kx = std::fpclassify(x);
   int ky = std::fpclassify(y);
   if (kx == FP_INFINITE) return (x>0)?1:-1;
   if (ky == FP_INFINITE) return (y>0)?-1:1;
   if (kx == FP_NAN && ky == FP_NAN) return 0;
   return (kx==ky)?0:(kx<ky)?-1:1;
}

Actually, I do know that it is not a really (mathematically speaking) total order (since e.g. bit-wise different NaN are all equal), but I am hoping it has the same (or a very close) behavior on several common architectures.

Any comments or suggestion?

(perhaps I should not care that much; and I deliberately don't care about signaling NaNs)

The overall motivation is that I am coding some dynamically typed interpreter which persists its entire memory state in JSON notation, and I want to be sure that the persistent state is stable between architectures, in other words if I load the JSON state and dump it, it stays idempotent for several architectures (notably all of x86-64, ia-32, ARM 32 bits...).

Community
  • 1
  • 1
Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547

1 Answers1

10

I would use:

int totalcompare(double x, double y) {
    int64_t rx, ry;

    memcpy(&rx, &x, sizeof rx);
    memcpy(&ry, &y, sizeof ry);

    if (rx == ry) return 0;

    if (rx < 0) rx ^= INT64_MAX;
    if (ry < 0) ry ^= INT64_MAX;

    if (rx < ry) return -1; else return 1;
 }

This makes 0.0 and -0.0 compare unequal, whereas if (x==y) return 0; in your version makes them compare equal, meaning that your version is only a preorder. NaN values are above the rest and different NaNs compare different. All values comparable for <= should be in the same order for the above relation.

Note: the above function is C. I do not know C++.

Pascal Cuoq
  • 79,187
  • 7
  • 161
  • 281
  • Are you sure it is not architecture dependent? (Can't `double` have an endianness different than `int64_t` ?) – Basile Starynkevitch Nov 22 '13 at 21:56
  • 1
    @BasileStarynkevitch It is possible in theory for floating-point values to be represented with a different endianness than integer ones, but there are many advantages to representing them with the same endianness. The natural inclination of any performance-conscious designer would be to make them always have the same endianness, even on bi-endian processors. Concerning ordinary architectures that everyone has, I can vouch for IA-32, x86-64, and PowerPC in both modes. I don't know about ARM. – Pascal Cuoq Nov 22 '13 at 22:08
  • Minor: 1) Would not a union of `int64_t, double` be more elegant? 2) When using `memcpy(a,b, sz);` of the supposed same size, I favor `memcpy(dest, src, sizeof *dest);` UB either way if sizes mis-match - just think this is easier to trouble-shoot. +1 for a fine solution. – chux - Reinstate Monica Jan 28 '16 at 23:00
  • @chux I have integrated the `sizeof` suggestion. The other one is really a matter of preference: just the other day a perfectly good answer was apologizing for using `union` for this (and no apology was necessary). – Pascal Cuoq Jan 28 '16 at 23:22
  • In any case, it is the neat `if (rx < 0) rx = (~rx) | INT64_MIN;` that caught my attention and is the crux of a nice answer. – chux - Reinstate Monica Jan 28 '16 at 23:25
  • @PascalCuoq `if (rx < 0) rx ^= INT64_MAX; // slightly more efficient?` – AlanK Mar 11 '19 at 16:45
  • 1
    @AlanK: that's a neat trick for 2's complement machines. You could do that branchlessly with `sar $tmp, $rx, 63` / `shr $tmp, 1` / xor $rx, $tmp` (arithmetic right shift to broadcast the sign bit, then logical right shift by 1 to get 0 or `0x7fff...`). But C doesn't have a portable way to express arithmetic right shifts. Compilers can often recognize `mask = (rx < 0) : -1ULL : 0;` as an an idiom for bit-broadcast with an arithmetic right shift, or whatever other machine-specific asm trick is useful (e.g. xor into a temporary and ALU select which result with a `cmov`. `xor` sets SF...). – Peter Cordes Mar 12 '19 at 04:06
  • 2
    @chux-ReinstateMonica I don't know about C but union type punning is explicit UB in C++. Your only options are `memcpy` or C++20's `bit_cast` IIRC – RecursiveExceptionException Sep 18 '20 at 22:17
  • 1
    @RecursiveExceptionException: C99 does guarantee union type-punning, unlike C++ where it's "only" supported as an extension by most mainstream compilers (including gcc/clang/MSVC/ICC). But yes, C++20 `bit_cast` is finally non-horrible portable syntax for type-punning, about time. – Peter Cordes Oct 08 '20 at 10:05