1

I saw some C++ code:

  float randfloat(float a, float b)
  {
    uint32_t val = 0x3F800000 | (rng.getU32() >> 9);
    float fval = *(float *)(&val);
    return a + ((fval - 1.0f) * (b - a));
  }

The complete code is on a Github gist. It seems to first bitwise OR the binary number 111111100000000000000000000000 with an unsigned random integer that is shifted right by 9 positions.

And then the bits are treated as a float, and made into a float? I would have guessed that it returns a float that is a random number between a and b, with a inclusive and b exclusive. So some other parts of the code uses randfloat(0.9, 0.85) and I think it is "almost" the same as randfloat(0.85, 0.9), except the first case is exclusive of 0.85, while the second case is exclusive of 0.9.

Does somebody know what is going on with the part 0x3F800000 | (rng.getU32() >> 9) and making it a float -- is it IEEE 754?

nonopolarity
  • 146,324
  • 131
  • 460
  • 740
  • 1
    Not completely related, but `*(float *)(&val);` invokes *undefined behavior*. So strictly speaking this entire function invokes UB – UnholySheep Aug 25 '20 at 20:31
  • 2
    Strictly speaking `float fval = *(float *)(&val)` is undefined behavior. The portable alternative would be to `memcpy` from `val` to `fval`. You should check that they are the same size with something like `static_assert(sizeof(val) == sizeof(fval));`. – François Andrieux Aug 25 '20 at 20:31
  • is this common used in certain platforms... this actually might be the code that runs on a Nintendo Switch... but the person who extracted the code might have ported it to the Intel processor and C++ that commonly can be compiled and run on a Mac or PC... – nonopolarity Aug 25 '20 at 20:33
  • Btw. have fun with [h-schmit](https://www.h-schmidt.net/FloatConverter/IEEE754.html) and you can most probably figure this out. – KamilCuk Aug 25 '20 at 20:37
  • 1
    This type of pointer casting to set arbitrary memory representation for floats is an old trick that is probably best known for the [Quake Fast Inverse Square Root](https://betterexplained.com/articles/understanding-quakes-fast-inverse-square-root/). It looks like the intention is to set the first 9 bits to 1 and randomize the rest of the bits. I believe in IEEE 754 the first bit is the sign, and the next 8 are the exponent. Assuming that is the target representation, it looks like it wants `fval` to be a random `float` but with all exponent and sign bits set. It is not a portable approach. – François Andrieux Aug 25 '20 at 20:38
  • Looks like someone was upset at the performance of the floating point random number generator and came up with this *concoction*. – tadman Aug 25 '20 at 20:41
  • That's why I like the caliber of C++ programmers... so using https://www.h-schmidt.net/FloatConverter/IEEE754.html, it seems it is to skip the left most 2 bits (have them as `0`), and then get a number from `1.0` to `1.9999999...`, and so `fval - 1.0f` is just to get a number from `0.0` to `0.9999....` – nonopolarity Aug 25 '20 at 20:45
  • 1
    But when Francois said "set the first 9 bits to 1" that was wrong, the two highest bits are zero. – Ben Voigt Aug 25 '20 at 20:45
  • @tadman true... couldn't the code just use `rand()` to get a number from `0.0` to `0.99999....` with `0` inclusive and `1` exclusive? – nonopolarity Aug 25 '20 at 21:29
  • It probably could, but I have a feeling that may have under-performed compared to this code. I'm not saying this code is great or even trustworthy, but I bet it's pretty fast. In C++ you'd tend to steer towards using the [random number generation facilities](https://en.cppreference.com/w/cpp/numeric/random) and stay far, far away from `rand()` which is not really random, not performant. – tadman Aug 25 '20 at 21:32
  • I see... so you are saying in general, always use the functions provided using `#include ` and some sample is at https://stackoverflow.com/questions/19665818/generate-random-numbers-using-c11-random-library – nonopolarity Aug 26 '20 at 21:47

3 Answers3

3

0x3f800000 is the binary representation of 1.0f when stored in the IEEE-754 binary32 single-precision format. This format has 23 stored significand (mantissa) bits. These bits are filled with the 23 high order bits returned by a 32-bit PRNG, resulting in a pseudo random number in [1, 2). Subtracting 1.0f from the result gives a random number in [0, 1), which is then scaled to the interval [a, b] or [a, b), depending on how rounding works out for a specific pair a, b.

As others have already pointed out in comments, the type-punning idiom used in this code to reiterpret a float as a uint32_t is unsafe, as it invokes undefined behavior.

njuffa
  • 23,970
  • 4
  • 78
  • 130
  • "0x3f800000 is the binary representation of 1.0f" - you're in my pub quiz team. +1. – Bathsheba Aug 25 '20 at 20:49
  • pub quiz team... like they play games in a bar drinking beer and ask quiz questions such as what is the capacity of a floppy disk used for Apple II? – nonopolarity Aug 26 '20 at 22:36
1

It looks like a rather pathetic attempt to retrieve a random number in the interval [a, b) which is perhaps unwittingly exponentially distributed depending on how the mantissa and exponent are arranged, and the endianness of uint32_t on your platform. The behaviour of the code is undefined due to the cast violating strict aliasing.

Bin it and replace with

std::random_device rd;
std::mt19937 e(rd());
std::uniform_real_distribution<float> dist(a, b);   
dist(e); // a drawing

This is portable and passes very many statistical tests of randomness. Of course, the first three statements are all to do with setting up the generator - don't call that repeatedly.

Bathsheba
  • 231,907
  • 34
  • 361
  • 483
1

The below attempts to form a random number in the range [1.0f...2.0f) by fixing the exponent and filling with a random significant.

uint32_t val = 0x3F800000 | (rng.getU32() >> 9);
float fval = *(float *)(&val);

Lots of UB.

A simple alternative is

float fval = (rng.getU32() >> 9)/8388608.0f;  // divide by 2^23

or

float fval = ldexpf(rng.getU32() >> (32-23), -23);  // divide by 2^23
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • I wonder if the OP appreciates that this does not generate uniformly distributed numbers? Nice analysis though, +1. – Bathsheba Aug 25 '20 at 21:27
  • @Bathsheba Given a fair `rng()`, I see this as a fair [0....1.0f) distribution. Once `* (b - a)` occurs, I agree with you: uniformity is reduced/lost. – chux - Reinstate Monica Aug 25 '20 at 21:30