3

TLDR; Does the following code invoke undefined (or unspecified) behaviour ?

#include <stdio.h>
#include <string.h>

void printme(void *c, size_t n)
{
  /* print n bytes in binary */
}

int main() {
  long double value1 = 0;
  long double value2 = 0;

  memset( (void*) &value1, 0x00, sizeof(long double));
  memset( (void*) &value2, 0x00, sizeof(long double));

  /* printf("value1: "); */
  /* printme(&value1, sizeof(long double)); */
  /* printf("value2: "); */
  /* printme(&value2, sizeof(long double)); */

  value1 = 0.0;
  value2 = 1.0;

  printf("value1: %Lf\n", value1);
  printme(&value1, sizeof(long double));
  printf("value2: %Lf\n", value2);
  printme(&value2, sizeof(long double));

  return 0;
}

On my x86-64 machine, the output depends on the specific optimization flags passed to the compiler (gcc-4.8.0, -O0 vs -O1).

With -O0, I get

value1: 0.000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 
value2: 1.000000
00000000 00000000 00000000 00000000 00000000 00000000 00111111 11111111
10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

While with -O1, I get

value1: 0.000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 
value2: 1.000000
00000000 00000000 00000000 00000000 00000000 01000000 00111111 11111111
10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 

Please note the extra 1 in the second last line. Also, uncommenting the print instructions after the memset makes that 1 disappear. This seems to rely on two facts:

  1. long double is padded, i.e., sizeof(long double) = 16 but only 10 bytes are used.
  2. the call to memset might be optimized away
  3. the padding bits of the long doubles might change without notice, i.e. floating point operations on value1 and value2 seems to scramble the padding bits.

I'm compiling with -std=c99 -Wall -Wextra -Wpedantic and get no warnings so I'm not sure this is a case of strict aliasing violation (but it might well be). Passing -fno-strict-aliasing doesn't change a thing.

The context is a bug found in HDF5 library described here. HDF5 does a some bit fiddling to figure out the native bit representation of floating point types, but it gets confused if the padding bits do not stay zero.

So:

  1. Is this undefined behaviour?
  2. Is this a strict aliasing violation?

Thanks.

edit: This is the code for printme. I admit I had just cut&pasted from somewhere without paying too much attention to it. If the fault is in here I'll go around the table with pants down.

void printme(void *c, size_t n)
{
  unsigned char *t = c;
  if (c == NULL)
    return;
  while (n > 0) {
    int q;
    --n;
    for(q = 0x80; q; q >>= 1) 
      printf("%x", !!(t[n] & q));
    printf(" ");
  }
  printf("\n");
}
andreabedini
  • 1,295
  • 1
  • 13
  • 20
  • 1
    I'm running gcc 4.7.2 and I observed identical output for `-O0` and `-O1`. – lurker Sep 07 '13 at 02:04
  • @mbratch yes, I am aware it depends on the compiler version too. Thanks. – andreabedini Sep 07 '13 at 02:07
  • 1
    I love the extended-precision `long double` and I am grateful that GCC makes it available, but I really hate GCC's choice to represent it with 16 bytes. – Pascal Cuoq Sep 07 '13 at 02:09
  • 1
    @PascalCuoq that can be influenced with the flag `-m96bit-long-double`. See http://gcc.gnu.org/onlinedocs/gcc/i386-and-x86_002d64-Options.html – andreabedini Sep 07 '13 at 02:17
  • @PascalCuoq: It's not GCC's choice but the x86_64 psABI's. Yes it's a bit annoying to have that much padding, but I don't see any other viable option. The 32-bit x86 ABI's failure to align `long double` resulted in serious performance impact on modern chips. – R.. GitHub STOP HELPING ICE Sep 07 '13 at 08:17

3 Answers3

2

While the C standard allows operations to clobber the padding bits, I don't think this is what's happening on your system. Rather, they're never being initialized to begin with, and GCC is simply optimizing out the memset at -O1, since the object is subsequently overwritten. This could probably be suppressed with -fno-builtin-memset.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
  • yes, I agree. But only the non-padding bits of the object are subsequently overwritten. You are right, -fno-builtin-memset fixes the problem (in this case). – andreabedini Sep 07 '13 at 03:03
  • Conceptually, writing to the object as `long double` writes the whole object, but the padding bits take on indeterminate value. The easiest, most efficient indeterminate value to implement is "whatever was already there". However, since it's indeterminate, the compiler is free to optimize out earlier stores. – R.. GitHub STOP HELPING ICE Sep 07 '13 at 03:49
  • @R..GitHubSTOPHELPINGICE: Unfortunately, the Standard fails to specify what is and is not guaranteed about indeterminate values which are accessed using character types. The Standard clearly intends that at least something be guaranteed about the behavior of programs accessing such values, or else it would be pointless to single out character-type objects whose address is taken, but the Standard doesn't guarantee semantics consistent with "Unspecified" values. – supercat Apr 27 '22 at 19:37
0

Is this undefined behaviour?

Yes. The padding bits are indeterminate(*). Accessing indeterminate memory might as well be undefined behavior (it was undefined behavior in C90 and some C99 compilers treat it as undefined behavior. Also the C99 rationale says that accessing indeterminate memory is intended to be undefined behavior. But the C99 standard itself does not say it so clearly, it only alludes to trap representations and may give the impression that if one knows one does not have trap representations, one can obtain unspecified values from indeterminate memory). The padding part of the long double is at the very least unspecified.

(*) C99's footnote 271 says “The contents of ‘‘holes’’ used as padding for purposes of alignment within structure objects are indeterminate.” The text earlier refers to unspecified bytes, but that's only because bytes do not have trap representations.

Is this a strict aliasing violation?

I do not see any strict aliasing violation in your code.

Pascal Cuoq
  • 79,187
  • 7
  • 161
  • 281
  • Thanks. Does it matter if inside printme `void *c` gets casted to `unsigned char *` for printing? Would that be a strict aliasing violation? – andreabedini Sep 07 '13 at 01:58
  • 3
    @beb0s Casting to `unsigned char *` is one of the correct ways to access the representation without breaking the aliasing rules. – Pascal Cuoq Sep 07 '13 at 01:59
  • I don't see anything uspecified or undefined here at all. `sizeof(long double)` is returning 16 on his machine, and he appears to be accessing 16 bytes. Of course, he's not showing us the code that does that, so we can't say for sure. – Lee Daniel Crocker Sep 07 '13 at 02:33
  • @LeeDanielCrocker When `sizeof(t)` computes as `N` for your compilation platform, it does not mean that you are allowed to read all `CHAR_BIT * N` bits from an object of type `t`. It is not true for structs, which can have padding, and it is not true for base types. Only `unsigned char` is guaranteed not to have any padding bits. – Pascal Cuoq Sep 07 '13 at 02:37
  • I'm certain that CHAR_BIT on his machine (an Intel) is 8, and GCC on Intel64 uses 128-bit long doubles. Padding is not his issue: the bits he shows don't resemble any IEEE float representation, either 10, 12, or 16 bytes. Clearly his `printme()` is broken, and his padding thoughts are just a red herring. – Lee Daniel Crocker Sep 07 '13 at 02:44
  • @LeeDanielCrocker You know that nearly all compilers for IA-32 and x86-64 that offer a `long double` type different from `double` actually offer the 80-bit extended-double type from the 8087 and pad it up to 12 or 16 bytes, right? – Pascal Cuoq Sep 07 '13 at 02:48
  • Mine does--10 bytes padded to 12, unless I tell it to use the real quad precision library. His sizeof() is 16, but you're right, it might well be using only 10-byte floats. But EVEN HIS FIRST 10 BYTES ARE WRONG. Padding is not his issue, his printme() isn't working. – Lee Daniel Crocker Sep 07 '13 at 02:52
  • @LeeDanielCrocker The printout looks fine to me: 128 bits from the furthest away to the nearest, starting with 6 bytes of padding, followed respectively by the big-endian transcription of what look very much like the 80-bit representations of 0.0 and 1.0. – Pascal Cuoq Sep 07 '13 at 03:00
  • @LeeDanielCrocker I posted the code for `printme()`. The bit representation looks fine to me (see en.wikipedia.org/wiki/Extended_precision) – andreabedini Sep 07 '13 at 03:04
  • @beb0s: Nothing is wrong with your `printme`. Lee is mistaken. – R.. GitHub STOP HELPING ICE Sep 07 '13 at 08:11
-2

I don't see anything undefined here, or even unspecified (two very different things). Yes, the memset() calls are optimized out. On my machine (i86-32) long double is 12 bytes, padded to 16 in structs and on the stack. On your machine, they are clearly full 16 bytes, since sizeof(long double) is returning 16. Neither of the "printme" outputs resemble proper IEEE 128-bit floating point format, so I suspect there are other bugs in the printme() function that aren't shown here.

Lee Daniel Crocker
  • 12,927
  • 3
  • 29
  • 55
  • The `printme` output shows the 80-bit extended-double that more or less correspond a 79-bit IEEE 754 format, as described here: http://en.wikipedia.org/wiki/Extended_precision – Pascal Cuoq Sep 07 '13 at 02:50
  • 1
    On your machine the 12-byte `long double` is 10 bytes of data and 2 bytes of padding. Look at `LDBL_MAX` from `float.h`. Or better yet, execute this program with your compiler: http://ideone.com/dY7ade – Pascal Cuoq Sep 07 '13 at 02:53
  • 1
    Ah...I see what's going on. The he's printing the bytes backwards: 6 bytes of padding first, then 10 bytes of value MSB to LSB, opposite of how they're stored. – Lee Daniel Crocker Sep 07 '13 at 03:08