Can I write const expression double that is two ulps less than -0.5
Asked Answered
M

2

5

I have a set of floating point calculation based on numbers I receive via a json packet. At the end of my calculation I require one of the numbers to be >= -0.5. I'm finding that sometimes I have a value that fails the test because it is one ULP below the threshold. Is there anyway to write a constexpression that means something like

constexpr auto threshold = -0.5 - 2*ULP;

or do I have to resort to something like

auto threshold = -0.5;
threshold = std::nexttoward(threshold, -2.0);
threshold = std::nexttoward(threshold, -2.0);
Mound answered 8/8, 2017 at 12:56 Comment(8)
-0.5 is represented exactly in floating-point. Unclear why any approximation should be required.Sturgeon
try constexpr auto threshold = -0.5f - 2.0f * std::numeric_limits<float>::epsilon();' (c++11)Buffalo
At the very least you can write this as const auto threshold = nexttoward(nexttoward(-0.5));Coheman
@ikleschenkov, I like that idea, but becuase epsilon is one ULP at 1.0, in this case it actually gives a value four ULPs below 0.5. If I take out the 2, it works in this case although it's not a very generic answer for any threshold value.Mound
@BartekBanachewicz -- make that an answer. It will, hopefully, squeeze out all those that play games with epsilon.Aweather
I'm confused. If the requirement is that the value be >= -0.5, why do you want to allow values that are less than -0.5?Aweather
@PeteBecker: A user chooses a value at the limit of the valid range. Some FP calculations are performed and the results are converted to a JSON string passed through a socket. The values are unpacked and more FP calculations are performed. FP calculations aren't exact and they sometimes produce a little round-off that pushes the value outside my valid range. I can't go back and tell the user to pick a valid value because he already did! So I want to handle this case without completely discarding the validation check.Mound
@JohnGordon -- I was making a slightly snarky point about your statement that you "require one of the numbers to be >= -0.5" but that you really want a lower threshold.Aweather
G
3

Maybe something like this can do it (it needs base 2 floating point representation, and it doesn't work for denormals):

constexpr double num = -0.5;
constexpr double threshold = num + 
          2.0 
        * (num < 0 ? -1 : 1) 
        * std::pow(2.0,
                   std::floor(std::log(std::abs(num)) / std::log(2.0))) 
        * std::numeric_limits<double>::epsilon();

How does it work (I describe it with IEEE754 in mind)?

Epsilon means 1 ULP when a number is in the range of [1.0;2.0). We need to scale epsilon, so it always means 1 ULP. The scale is based on the exponent part of the floating point number. If the number is [1.0;2.0), then the scale must be 1. If the number is [2.0;4.0), then the scale must be 2, for [4.0;8.0), it must be 4, etc. So, we need to find the nearest, less-than-or-equal power of 2: it is 2^floor(log2(number)). And we need to care of negative numbers, that's why the abs and (num<0?-1:1) in the formula.

Geosphere answered 8/8, 2017 at 13:42 Comment(2)
I'm accepting this as the answer because it's a constexpr and works across different ranges of values.Mound
I discovered while implementing this that a std::log2() function is now available (gcc 6.2.1). That cleans it up quite a bit if you replace std::log(std::abs(num))/std::log(2.0) with std::log2(std::abs(num))Mound
L
4

You should be able to achieve desired threshold with epsilon, something like

constexpr auto threshold = -0.5 - std::numeric_limits<double>::epsilon();

Possibly add *2 if you think you really need it, though since epsilon is defined for value 1.0, it might work out just right for you here for 0.5.


Alternatively, just don't use it as constexpr. Unless it is some inner loop in some very performance sensitive code, the difference should be negligible:

const auto threshold = std::nexttoward(std::nexttoward( -0.5, -2.0), -2.0);
Lorikeet answered 8/8, 2017 at 13:14 Comment(7)
It's important to realize that std::numeric_limits<double>::epsilon() is not the same as one ULP except around 1.0. If my threshold was -5.0 instead of -0.5, epsilon() would be less than one ULP and the threshold stays exactly equal to 5.0. In this case, the line you suggested gives me a value of two ULPs below -0.5 without having to multiply by two.Mound
@JohnGordon: It is the same as one ULP in the range of [1.0;2.0) (considering IEEE754 format). So the first part of this answer isn't right.Geosphere
@geza: Actually, the first part is correct because I asked for 2 ULPs and epsilon() is 2 ULPs at +/- 0.5, although I don't think that was intentional in the answer.Mound
@JohnGordon: Yes, you're right. For some reason I thought that -0.5 is just an example, and you need a general solution.Geosphere
@geza: I asked a specific question, but I would like a more general solution. Your answer is more general, but looking at it makes my eyes bleed just a little.Mound
@JohnGordon: don't say that, it is not that ugly :) I've added a little description.Geosphere
@JohnGordon I edited geza's answer a bit, maybe looking at it hurts a bit less now...Lorikeet
G
3

Maybe something like this can do it (it needs base 2 floating point representation, and it doesn't work for denormals):

constexpr double num = -0.5;
constexpr double threshold = num + 
          2.0 
        * (num < 0 ? -1 : 1) 
        * std::pow(2.0,
                   std::floor(std::log(std::abs(num)) / std::log(2.0))) 
        * std::numeric_limits<double>::epsilon();

How does it work (I describe it with IEEE754 in mind)?

Epsilon means 1 ULP when a number is in the range of [1.0;2.0). We need to scale epsilon, so it always means 1 ULP. The scale is based on the exponent part of the floating point number. If the number is [1.0;2.0), then the scale must be 1. If the number is [2.0;4.0), then the scale must be 2, for [4.0;8.0), it must be 4, etc. So, we need to find the nearest, less-than-or-equal power of 2: it is 2^floor(log2(number)). And we need to care of negative numbers, that's why the abs and (num<0?-1:1) in the formula.

Geosphere answered 8/8, 2017 at 13:42 Comment(2)
I'm accepting this as the answer because it's a constexpr and works across different ranges of values.Mound
I discovered while implementing this that a std::log2() function is now available (gcc 6.2.1). That cleans it up quite a bit if you replace std::log(std::abs(num))/std::log(2.0) with std::log2(std::abs(num))Mound

© 2022 - 2024 — McMap. All rights reserved.