Floor and Ceil Versus Denormals on CPU and GPU
Programming, graphics, games, media, C++, Windows, Internet and more...
Sorry, you need Javascript on to email me.
-->
Main PageBlogProductionsAbout
Blog "
Floor and Ceil Versus Denormals on CPU and GPU
Sat
23
May 2026
Recently, I dove deep into floating-point numbers and their behavior. Somehow, this topic haunts me in my programming practice since I created Floating-Point Formats Cheatsheet back in 2013 and also released a comprehensive article The Secrets of Floating-Point Numbers in 2024.
This time, I would like to focus on one specific question:
What is the result of floor(-1.175493930432748e-38) ?
Note: Hexadecimal value of our input number is 0x807FFFFD.
Floor, ceil, trunc, round
To recap, floor, ceil, trunc, round are functions available in the standard library of C, C++, as well as shading languages: HLSL and GLSL. Each of them transforms a floating-point number into an integral floating-point value, but using different rounding rules.
Note this is not about a conversion from float to int. The result of these functions is still a float, just having only integral part. When the input is already integral, the value is returned as-is. Otherwise, it gets "snapped" to the nearest integer in a specific direction:
floor - rounding "down" i.e., towards -infinity.
ceil - rounding "up" i.e., towards +infinity.
trunc - rounding towards zero, which we can also explain as truncating the fractional part.
round - rounding to the nearest integer up or down, depending on which one is closer.
Examples:
Note: IEEE 754 floating-point numbers distinguish between positive and negative zero, so some results below are -0.0 rather than 0.0 to visualize this distinction.
floor( 5.7) = 5.0 ceil( 5.7) = 6.0 trunc( 5.7) = 5.0 round( 5.7) = 6.0<br>floor( 0.2) = 0.0 ceil( 0.2) = 1.0 trunc( 0.2) = 0.0 round( 0.2) = 0.0<br>floor(-0.2) = -1.0 ceil(-0.2) = -0.0 trunc(-0.2) = -0.0 round(-0.2) = -0.0<br>floor(-5.7) = -6.0 ceil(-5.7) = -5.0 trunc(-5.7) = -5.0 round(-5.7) = -6.0
When talking about round, there is also a question what happens when we are exactly halfway, like round(2.5). Various programming languages define it differently:
Standard C/C++ function defines it to round away from zero, so round(2.5) = 3.0, round(-3.5) = -4.0.
HLSL function defines it to round towards nearest even number, so round(2.5) = 2.0, round(-3.5) = -4.0.
GLSL leaves the behavior of the function round implementation-dependent, while also offering function roundEven that rounds towards nearest even.
Knowing all this, we can answer our main question:
According to mathematical rules, floor(-1.175493930432748e-38) = -1.0, because the number is between -1 and 0.
Denormals
However, those of you who know more about the structure of floating-point numbers may notice that our input value is a subnormal. Subnormal numbers , also called denormalized numbers or denormals , are values so small (so close to 0) that they use a special representation where the implicit leading 1 bit is no longer assumed. They have exponent = 0. The minimum positive normalized value representable by 32-bit floats is 1.18 * 10^-38, while the minimum value representable as denormalized is 1.4 * 10^-45, so our number falls in that range.
We wouldn't need to care about denormals if not for the fact that:
some platforms preserve them (processing the values they represent),
while others "flush" them to zero (treating them as if the number was exactly 0).
This is the problem I stumbled upon recently. In most cases, it doesn't matter. For example, when rendering graphics, the difference between such a small number and 0 would produce an indistinguishable difference in results. After applying functions such as floor and ceil, however, the difference is significant:
If the platform preserves denormals:
floor(-1.175493930432748e-38) = -1.0 ceil(-1.175493930432748e-38) = -0.0<br>floor( 1.175493930432748e-38) = 0.0 ceil( 1.175493930432748e-38) = 1.0
If the platform flushes denormals to 0:
floor(-1.175493930432748e-38) = -0.0 ceil(-1.175493930432748e-38) = -0.0<br>floor( 1.175493930432748e-38) = 0.0 ceil( 1.175493930432748e-38) = 0.0
The behavior of a specific platform may depend on many factors, such as flags used during compilation of our source code, as well as some floating-point modes controlled in runtime. It may be an unexpected source of nondeterminism between CPU and GPU, as well as between GPU vendors.
I've performed a few tests. Here are my results:
CPU in x86 64-bit architecture (AMD Ryzen 7 7800X3D, but I don't expect differences between AMD and Intel here) on Windows, executing C++ code compiled using Visual Studio 2022 appeared to preserve denormals when doing floor and ceil. I've tested the following options, with no change in the results:
Both Release and Debug configurations (with and without compiler optimizations)
With Floating Point Model parameter set to /fp:precise, /fp:strict, /fp:fast
With and...