Floor and Ceil versus Denormals on CPU and GPU

ibobev1 pts0 comments

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...

floor ceil round 175493930432748e denormals floating

Related Articles