4

Converting a uint8 value to a normalized float is pretty straight forward:

uint8 i = 0xAF;
float f = float(i)/float(0xFF);

But this feels more expensive than it should be...

Is there a way of making this conversion that's more efficient? I ask mostly out of curiosity, but also because my 3D program makes this conversion a significant number of times.

Readability isn't important, and the uint8 would always span the entire range

0 == 0.f and 255 == 1.f

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
Anne Quinn
  • 12,609
  • 8
  • 54
  • 101
  • 2
    I suspect this is the best you can do. This is something a compiler can easily optimize. You might compare the performance to using a lookup table. It's possible under certain usage patterns that could be faster. – Vaughn Cato Jan 24 '16 at 17:21
  • 1
    How about computing 1/255 and then using multiplication? – nsilent22 Jan 24 '16 at 17:35
  • 1
    @nsilent22 beat me to it: Certainly replace the division by a multiplication. See my answer to a similar question here: http://stackoverflow.com/a/34517727/3530129. – Reto Koradi Jan 24 '16 at 17:37
  • @RetoKoradi - that looks promising, the division was the worrying bit. `n/255.f == n * (1.f/255.f)` will hold true, right? (I'd like to think floating points aren't THAT bad, but you never know) – Anne Quinn Jan 24 '16 at 17:48
  • The compiler will not necessarily make that replacement, since it can produce a slightly different result due to different rounding. I commented on that towards the end of my answer. See also the answers to the linked question: http://stackoverflow.com/questions/1366437/optimizing-a-floating-point-division-and-conversion-operation. – Reto Koradi Jan 24 '16 at 17:53
  • 4
    Pre-compute a table with 256 entries. Use the integer as a lookup index. This way, no floating-point math at run time at all. – Igor Tandetnik Jan 24 '16 at 17:55
  • Change your math to use fixed point expressions. Conversion is performed at input and output. I like to use denominators (bases) that are powers of 2 so I can use binary shifting instead of division or multiplication. – Thomas Matthews Jan 24 '16 at 18:04
  • @Clairvoire: `x / 255.0f` is very different from `x * (1.0f / 255.0f)`. – tmyklebu Jan 24 '16 at 18:31
  • @tmyklebu - Yeah, I tested it with an online compiler and it's a bit off for most of the input range. http://cpp.sh/5j6n However, they all convert back to the respective input in the end, so I can live with that – Anne Quinn Jan 24 '16 at 18:46

2 Answers2

2

Here are a couple of dirty tricks that may do what you like on common platforms if you switch off your compiler's strict aliasing rules:

float tofloat(uint8_t x) {
  uint32_t foo = 0x3f800000 + x * 0x8080;
  return (float &)foo + 256 - 257;
}

float tofloat(uint8_t x) {
  uint32_t foo = 0x3f800000 + x * 0x8080 + (x+1) / 2;
  return (float &)foo - 1;
}
tmyklebu
  • 13,915
  • 3
  • 28
  • 57
  • What is the purpose of writing `+ 256 - 257` instead of `- 1`? – dshin Jan 24 '16 at 18:26
  • @dshin: I want to round the value. `tofloat(255)` winds up being substantially wrong---off by something like 1.5e-5---if you don't. – tmyklebu Jan 24 '16 at 18:31
  • Sorry, I'm still a bit confused. If `tofloat(255)` evaluates to 1.0, then wouldn't that mean that `(float &)foo` evaluates to 2.0? I would try to investigate this on my own but on my platform the first `tofloat()` doesn't seem to work. – dshin Jan 24 '16 at 19:04
  • Aha. I transcribed it incorrectly; I had an off-by-one. Does it work now? Adding 256 and subtracting 257 effectively gives a coarser rounding of `foo`. – tmyklebu Jan 24 '16 at 19:07
  • Yes, it works now, and indeed the behavior changes when changing ` + 256 - 257` with `- 1`. I presume there is float overflow going on? – dshin Jan 24 '16 at 19:08
  • 1
    It might be educational for you to look at the output of `printf("%a %a %a\n", (float &)foo-1, (float &)foo+256, (float &)foo+256-257);` It's substantially easier to see this sort of thing when using hexadecimal floating-point notation. – tmyklebu Jan 24 '16 at 19:11
  • Thanks, that was indeed educational. – dshin Jan 24 '16 at 19:17
  • Can gcc -ffast-math mess with the accuracy of your `tofloat()`? I tried using -ffast-math and it seemed ok, but from my understanding it gives gcc the option to assume associativity on some float operations. – dshin Jan 24 '16 at 19:20
  • I don't know what `-ffast-math` does. I just assume it's equivalent to `-fbreak-my-carefully-written-code -fbreak-fast-hacky-things`, though I have no direct experience to back that up. – tmyklebu Jan 24 '16 at 19:34
  • I verified that -ffast-math -O3 changes the return value of `tofloat()`. – dshin Jan 26 '16 at 03:07
1

Here are the operations that I see:

At compile time:

  • Convert 0xFF to a floating point constant.

At runtime:

  1. Convert i to floating point and store in temporary variable (or register).
  2. Divide the floating point value of i by the floating point constant.
  3. Assign the result of the division to the floating point variable f.

Bottlenecks:

The bottleneck is in the division. Division takes a long time, period (regardless of how it is implemented).

The next major bottleneck may be the conversion of integer to float. Some processors may have single instructions that will perform this; otherwise a software function will be executed (usually faster than the division).

To optimize:

  1. Get rid of the division. Use another method such as shifting or table lookup.
  2. Minimize the floating point conversion. Only convert as necessary, usually at input and output. Stay in integral or stay in floating point.

Notes:

  • Hardcoded constants are fast -- compiler stores in memory and execution takes from memory. Compiler doesn't need to calculate.
  • Constant expressions are faster, but slow down the compilation (probably negligible). The compiler performs the calculation and places the result in the executable.
  • Multiplication is usually faster than division.
  • Integral math is usually faster than floating point because floating point format needs to be separated, before calculations, then recombined afterwords (even in hardware, there is more work than simple integral operations).
Thomas Matthews
  • 56,849
  • 17
  • 98
  • 154