Dr.Kameleon
Dr.Kameleon

Reputation: 22820

Store signed 32-bit in unsigned 64-bit int

Basically, what I want is to "store" a signed 32-bit int inside (in the 32 rightmost bits) an unsigned 64-bit int - since I want to use the leftmost 32 bits for other purposes.

What I'm doing right now is a simple cast and mask:

#define packInt32(X) ((uint64_t)X | INT_MASK)

But this approach has an obvious issue: If X is a positive int (the first bit is not set), everything goes fine. If it's negative, it becomes messy.


The question is:

How to achieve the above, also supporting negative numbers, in the fastest and most-efficient way?

Upvotes: 0

Views: 2157

Answers (5)

dash-o
dash-o

Reputation: 14493

Assuming that the only operation on the 64 bit value will be to convert it back to 32 (and potentially, storing/displaying it), there is no need to apply a mask. The compiler will sign extend the 32 bit attributes when casting it to 64 bit, and will pick the lowest 32 bit when casting the 64 bit value back to 32 bit.

#define packInt32(X) ((uint64_t)(X))
#define unpackInt32(X) ((int)(X))

Or better, using (inline) functions:

inline uint64_t packInt32(int x) { return ((uint64_t) x) ; }
inline int unpackInt32(uint64_t x) { return ((int) x) ; }

Upvotes: 0

dbush
dbush

Reputation: 224917

You need to mask out any bits besides the low order 32 bits. You can do that with a bitwise AND:

#define packInt32(X) (((uint64_t)(X) & 0xFFFFFFFF) | INT_MASK)

Upvotes: 5

Gerhardh
Gerhardh

Reputation: 12404

The "mess" you mention happens because you cast a small signed type to a large unsigned type. During this conversion the size is adjusted first with applying sign extension. This is what causes your trouble.

You can simply cast the (signed) integer to an unsigned type of same size first. Then casting to 64 bit will not trigger sign extension:

#define packInt32(X) ((uint64_t)(uint32_t)(X) | INT_MASK)

Upvotes: 9

Gem Taylor
Gem Taylor

Reputation: 5635

One option is to untangle the sign-extension and the upper value when it is read back, but that can be messy.

Another option is to construct a union with a bit-packed word. This then defers the problem to the compiler to optimise:

union {
  int64_t merged;
  struct {
     int64_t field1:32,
             field2:32;
  };
};

A third option is to deal with the sign bit yourself. Store a 15-bit absolute value and a 1-bit sign. Not super-efficient, but more likely to be legal if you should ever encounter a non-2's-complement processor where negative signed values can't be safely cast to unsigned. They are rare as hens teeth, so I wouldn't worry about this myself.

Upvotes: 1

Petr Skocik
Petr Skocik

Reputation: 60127

A negative 32-bit integer will get sign-extended into 64-bits.

#include <stdint.h>
uint64_t movsx(int32_t X) { return X; }

movsx on x86-64:

movsx:
        movsx   rax, edi
        ret

Masking out the higher 32-bits will remove cause it to be just zero-extended:

#include <stdint.h>
uint64_t mov(int32_t X) { return (uint64_t)X & 0xFFFFFFFF; }
//or uint64_t mov(int32_t X) { return (uint64_t)(uint32_t)X; }

mov on x86-64:

mov:
        mov     eax, edi
        ret

https://gcc.godbolt.org/z/fihCmt

Neither method loses any info from the lower 32-bits, so either method is a valid way of storing a 32-bit integer into a 64-bit one.

The x86-64 code for a plain mov is one byte shorter (3 bytes vs 4). I don't think there should be much of a speed difference, but if there is one, I'd expect the plain mov to win by a tiny bit.

Upvotes: 1

Related Questions