0

Recently I tried to make a "clever" macro for float (32bit) division with rounding up to unsigned int (32bit):

#include "float.h"
#include "stdint.h"

#define DIV_ROUND_UP(x, y)          (uint32_t)(((float)(x) / (float)(y)) + (float)(1.0f - FLT_EPSILON))

The FLT_EPSILON was used, to add something less than 1, so DIV_ROUND_UP(10u, 1u) will result in 10u, not 11u.

I was very surprised when DIV_ROUND_UP(10u, 1u) actually returned 11u. I checked experimentally that I have to use 5 * FLT_EPSILON to get 10u, but I still don't understand why. I assumed that if I add something less than 1, it will be truncated during conversion to uint32_t. Could anyone explain why it is not truncated? And why 5 * FLT_EPSILON works?

EDIT: I end up with the following solution, which works for positive numbers:

#define DIV_ROUND_UP(x, y) (((float)(x) / (float)(y)) > (uint32_t)((float)(x) / (float)(y)) ? \
(uint32_t)((float)(x) / (float)(y)) + 1u : \
(uint32_t)((float)(x) / (float)(y)))

1 Answers1

4

FLT_EPSILON is the distance between 1 and the next representable number. In a binary-based floating-point format, the representable numbers between 1 and 2 are spaced a distance FLT_EPSILON apart, and the numbers between 2 and 4 are twice that distance apart, and the numbers between 4 and 8 are four times FLT_EPSILON apart, and the numbers between 8 and 16 are eight times FLT_EPSILON apart, and so on. This is because a floating-point number is represented as a significand multiplied by a scale. FLT_EPSILON is the distance between numbers at the scale used for the number 1, so the distances between numbers at other scales are proportional to their scales.

At 10, the distance between representable numbers is 8•FLT_EPSILON. When you add 1.0f - FLT_EPSILON to 10, the real-number-arithmetic result would be 11 − FLT_EPSILON. But that number is not representable because the representable numbers are 8•FLT_EPSILON apart; a single FLT_EPSILON is too small a difference. The nearest representable numbers to 11 − FLT_EPSILON are 11 and 11 − 8•FLT_EPSILON. The default rule for producing a result is to produce the nearest representable number in either direction. Since 11 is closer to 11 − FLT_EPSILON than 11 − 8•FLT_EPSILON is, 11 is produced as the result.

When you add 1.0f - 5 * FLT_EPSILON, the real-number result would be 11 − 5•FLT_EPSILON. This is closer to 11 − 8•FLT_EPSILON than 11 is, so 11 − 8•FLT_EPSILON is produced as the result of the subtraction. Then converting that to uint32_t truncates, producing 10.

To get floating-point division to round up in many cases, simply use ceil: #define DIV_ROUND_UP(x, y) ((uint32_t) ceil((float) (x) / (float) (y))). This may not work in certain cases where x/y is so slightly above an integer that the floating-point division rounds to that integer before the ceil function operates.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • Thanks for explanation. I cannot use any function as I need this macro for compilation time constants. – radoslav006 Aug 04 '22 at 13:40
  • Here is my quick fix idea if someone will be interested (it is intended for positive numbers only): ``` #define DIV_ROUND_UP(x, y) (((float)(x) / (float)(y)) > (uint32_t)((float)(x) / (float)(y)) ? (uint32_t)((float)(x) / (float)(y)) + 1u : (uint32_t)((float)(x) / (float)(y))) ``` – radoslav006 Aug 04 '22 at 13:51
  • 2
    @radoslav006: It's better to either edit your question, or create your own answer. It's easy for answers in comments to be overlooked. – sj95126 Aug 04 '22 at 13:59
  • You are right, I will add that to my question. – radoslav006 Aug 05 '22 at 07:30
  • Eric Postpischil, "FLT_EPSILON is the distance between 1 and the next representable number." is more clear as the next _greatest_ number. – chux - Reinstate Monica Aug 08 '22 at 02:54