Building a 32-Bit Float Out of Its 4 Composite Bytes

Building a 32-bit float out of its 4 composite bytes

You could use a memcpy (Result)

float f;
uchar b[] = {b3, b2, b1, b0};
memcpy(&f, &b, sizeof(f));
return f;

or a union* (Result)

union {
float f;
uchar b[4];
} u;
u.b[3] = b0;
u.b[2] = b1;
u.b[1] = b2;
u.b[0] = b3;
return u.f;

But this is no more portable than your code, since there is no guarantee that the platform is little-endian or the float is using IEEE binary32 or even sizeof(float) == 4.

(Note*: As explained by @James, it is technically not allowed in the standard (C++ §[class.union]/1) to access the union member u.f.)

Building 32bit Float from 4 Bytes [Binary File I/O] in C++

So with a bit of effort and Igor's comment I was able to solve the problem. The following function reads everything into a buffer vector.

vector<char> buffer;

void fill() {
string filename = "";
cout << "Please enter a filename:\n>";
getline(cin, filename);

ifstream file(filename.c_str());

if (file) {
file.seekg(0,std::ios::end);
streampos length = file.tellg();
cout<< length << endl;
file.seekg(0,std::ios::beg);
file.seekg(540,'\0');

length-=540;
buffer.resize(length);
file.read(&buffer[0],length);
}
}

Then later on I call bytesToFloat in a loop. The endian-ness of bytesToFloat was incorrect so has now been reversed and it outputs the same values as my original file (I made my random file generator output a plain text version for comparison).

Does accessing the 4 bytes of a float break C++ aliasing rules

Is this actually valid C++ code?

Potentially yes. It has some pre-conditions:

  • std::uint8_t must be an alias of unsigned char
  • sizeof(float) must be 4
  • bytes + 3 mustn't overflow a buffer.

You can add a checks to ensure safe failure to compile if the first two don't hold:

static_assert(std::is_same_v<unsigned char, std::uint8_t>);
static_assert(sizeof(float) == 4);

I'm not sure whether it violates any aliasing rules.

unsigned char is excempted of such restrictions. std::uint8_t, if it is defined, is in practice an alias of unsigned char, in which case the shown program is well defined. Technically that's not guaranteed by the rules, but the above check will handle the theoretical case where that doesn't apply.



float is guaranteed to be at least 32 bits long.

It must be exactly 32 bits long for the code to work. It must also have exactly the same bit-level format as was on the system where the float was serialised. If it's standard IEE-754 single precision on both ends then you're good; otherwise all bets are off.

Convert a float to 4 uint8_t

You normally do this by casting the float to an array of uint8_t.

In C you can do it like this:

uint8_t *array;
array = (unit8_t*)(&f);

in C++ use the reinterpret_cast

uint8_t *array;
array = reinterpret_cast<uint8_t*>(&f);

Then array[0], ..., array[3] are your bytes.

Techniques for reinterpreting the bits of one type as a different type

Thanks to @geza for pointing out that the "failures" were an artifact of printf not being able to show very small numbers in %f format. With %e format the correct values are displayed.

In my tests, all of the various conversion methods discussed work, even the undefined behavior. Considering simplicity, performance, and portability it seems the solution using a union is best:

uint32_t bitPattern; 
volatile float bitPatternAsFloat;

union UintFloat {
uint32_t asUint;
float asFloat;
} uintFloat;

do {
bitPattern = i; // i is loop index
uintFloat.asUint = bitPattern;
bitPatternAsFloat = uintFloat.asFloat;

An interesting question is whether the float member in the union should be declared volatile: it is never explicitly written, so might the compiler cache the initial value and reuse it, failing to notice that the corresponding uint32_t is being updated? It appears to work OK on my current compiler without the volatile declaration, but is this a potential 'gotcha'?

P.S. I just discovered [reinterpret_cast][1] and wonder if that's the best solution.

SSE intrinsics: Convert 32-bit floats to UNSIGNED 8-bit integers

There is no direct conversion from float to byte, _mm_cvtps_pi8 is a composite. _mm_cvtps_pi16 is also a composite, and in this case it's just doing some pointless stuff that you undo with the shuffle. They also return annoying __m64's.

Anyway, we can convert to dwords (signed, but that doesn't matter), and then pack (unsigned) or shuffle them into bytes. _mm_shuffle_(e)pi8 generates a pshufb, Core2 45nm and AMD processors aren't too fond of it and you have to get a mask from somewhere.

Either way you don't have to round to the nearest integer first, the convert will do that. At least, if you haven't messed with the rounding mode.

Using packs 1: (not tested) -- probably not useful, packusdw already outputs unsigned words but then packuswb wants signed words again. Kept around because it is referred to elsewhere.

cvtps2dq xmm0, xmm0  
packusdw xmm0, xmm0 ; unsafe: saturates to a different range than packuswb accepts
packuswb xmm0, xmm0
movd somewhere, xmm0

Using different shuffles:

cvtps2dq xmm0, xmm0  
packssdw xmm0, xmm0 ; correct: signed saturation on first step to feed packuswb
packuswb xmm0, xmm0
movd somewhere, xmm0

Using shuffle: (not tested)

cvtps2dq xmm0, xmm0
pshufb xmm0, [shufmask]
movd somewhere, xmm0

shufmask: db 0, 4, 8, 12, 80h, 80h, 80h, 80h, 80h, 80h, 80h, 80h, 80h, 80h, 80h, 80h


Related Topics



Leave a reply



Submit