9

Recently, we discovered odd behavior in some old code. This code has worked for ages, but broke on some platform (XBox 360, PowerPC) with compiler optimizations turned on max. Usually, I'd suspect undefined behavior.

Code looks roughly like this:

#include <stdint.h>
uint32_t sign_extend16(uint32_t val)
{
   return (int32_t)(int16_t)val;
}

It's part of an emulator so the operation in question shouldn't be too strange. Normally, I'd expect this to only consider the lower 16-bits and sign-extend that to 32-bits. Apparently, this was the behavior it had for ages. On x86_64, GCC gives me this result:

0000000000000000 <sign_extend16>:
   0:   0f bf c7                movswl %di,%eax
   3:   c3                      retq

However, from what I could understand of the standard, converting an unsigned to a signed is not defined should it not be possible to represent the value of the unsigned one with the signed type.

Could it then be possible for the compiler to assume that the unsigned value would have to be in the range of [0, 32767], as any other value would be undefined? In that case, a cast to int16_t and yet another cast to int32_t would do nothing. In this case, would it be legal for the compiler to translate the code to a simple move?

Maister
  • 4,978
  • 1
  • 31
  • 34
  • 1
    The behavior of `(int16_t)val` is never undefined. Its behavior is well-defined if `val` is representable as an `int16_t`, otherwise the behavior is implementation-defined. – James McNellis Feb 09 '12 at 23:25
  • @Maister what is exactly the issue you have on x86_64? `movswl` instruction does sign extension. What is the result you have when you pass the value 32768? On your 32-bit/64-bit systems with `gcc`, the return value should be `0xFFFF8000`. – ouah Feb 09 '12 at 23:58
  • I might not have been clear enough. Behavior on x86_64 is what is expected. It does not however act as expected on xbox 360. – Maister Feb 10 '12 at 00:02
  • @Maister what is the compiler on the xbox 360, and what value is returned for argument 32768 ? – ouah Feb 10 '12 at 00:06
  • it *might* help if you explicitly truncate `val` as a first step via the cast `(uint16_t)`; if that doesn't do it, you should try to use type-punning for the conversion `uint16_t` -> `int16_t`... – Christoph Feb 10 '12 at 01:01

4 Answers4

9

A conversion between two integer types is never undefined behavior.

But some integer conversions are implementation defined.

On integer conversions C says:

(C99, 6.3.1.3p3) "Otherwise, the new type is signed and the value cannot be represented in it; either the result is implementation-defined or an implementation-defined signal is raised."

what does gcc on this case is documented here:

http://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html

"For conversion to a type of width N, the value is reduced modulo 2^N to be within range of the type; no signal is raised"

ouah
  • 142,963
  • 15
  • 272
  • 331
2

As ouah says, conversion of an out-of-range value gives an implementation-defined result (or allows an implementation-defined signal to be raised).

For example, it would be perfectly legal for an implementation to say that a conversion of an out-of-range value to int16_t only preserves the lower 15 bits of the value, and always sets the sign bit to 0. Thus it would interpret your sign_extend16() function as simply return val & 0x7fff;.

However, an implementation can't interpret your function such that it simply returns val unchanged - the implementation-defined conversion to int16_t must result in a value somewhere in the range of int16_t, so the final result must lie somewhere in [0, 32767] or [4294934528, 4294967295].

Note also that the int32_t cast there is completely superfluous.

Two alternatives which don't rely on implementation-defined conversions are (note the change of argument type of val):

uint32_t se16(uint16_t val)
{
    return -((uint32_t)val << 1 & 0x10000) | val;
}


uint32_t se16(uint16_t val)
{
    return (val ^ (uint32_t)32768) - (uint32_t)32768;
}

...but unfortunately the gcc optimiser doesn't seem to notice that these are just a sign-extension of the lower 16 bits.

Community
  • 1
  • 1
caf
  • 233,326
  • 40
  • 323
  • 462
  • What about `((int32_t)val - 32768) ^ (int32_t)(-32768)`? – supercat Aug 26 '13 at 18:23
  • @supercat: Yes, that works too, as does `(val ^ (uint32_t)32768) - (uint32_t)32768`. I can't get the optimiser to produce a single `movswl` with any of them, though. – caf Aug 27 '13 at 05:57
0

The two versions I've already mentioned in the comments:

#include <stdint.h>

uint32_t sign_extend16_a(uint32_t val)
{
    return (uint32_t)(int16_t)(uint16_t)val;
}

uint32_t sign_extend16_b(uint32_t val)
{
    union { uint16_t u; int16_t i; } ui;
    ui.u = (uint16_t)val;
    return (uint32_t)ui.i;
}

Produces the following output with gcc 4.5.3 on x86-64 with -O1:

.globl sign_extend16_a
    .def    sign_extend16_a;    .scl    2;  .type   32; .endef
sign_extend16_a:
    subq    $8, %rsp
    movswl  %cx, %eax
    addq    $8, %rsp
    ret
.globl sign_extend16_b
    .def    sign_extend16_b;    .scl    2;  .type   32; .endef
sign_extend16_b:
    subq    $8, %rsp
    movswl  %cx, %eax
    addq    $8, %rsp
    ret
Christoph
  • 164,997
  • 36
  • 182
  • 240
-1

Using union :

uint32_t sign_extend16(uint32_t val){
    union{
        uint32_t a;
        int32_t b;
        int16_t c;
    }o;
    o.a=val;
    o.b=o.c;
    return o.a;
}
cendar
  • 34
  • 3