2
\$\begingroup\$

What do you think about this implementation of 2 floats comparison functor considering how tolerance is introduced?

class Less
{
private:
    float m_tolerance;

public:
    Less(const float tolerance)
        : m_tolerance(tolerance)
    {

    }

    bool operator()(const float f1, const float f2) const
    {
        const bool toCloseToCompareSmaller = (std::abs(f2 - f1) < m_tolerance);
        const bool isSmaller = (f1 < f2);
        return !toCloseToCompareSmaller && isSmaller;
    }

    ~Less()
    {
    }
};
\$\endgroup\$
  • \$\begingroup\$ why not f1 < f2? as far as i know, problem in comparison exist only for check on equality. \$\endgroup\$ – user110702 Jul 14 '16 at 12:13
  • \$\begingroup\$ If you have some range when you say they are too close and though they are not equal they can be considered equal, analogically I thought within that range you should not be able to say one is smaller than the other :) \$\endgroup\$ – Narek Jul 14 '16 at 12:21
3
\$\begingroup\$

A few remarks:


Seems appropriate to add a default value of 0.0f in the constructor:

Less(const float tolerance = 0.0f)
    : m_tolerance(tolerance)
{

}

This class could most certainly be used as a base class.

For example:

class Less1 : public Less
{
public:
    Less1():Less(1.0f)
    {
        ...
    }
    ...
}

So you may as well make it suitable to serve as such:

  • Declare virtual ~Less()
  • Change private to protected where needed

The operation f1 < f2 is most likely less expensive than abs(f2 - f1).

So you may as well check it first:

bool operator()(const float f1, const float f2) const
{
    return f1 < f2 && std::abs(f2 - f1) >= m_tolerance;
}

You should add an assertion on the value of m_tolerance being non-negative.

Alternatively, you could use std::abs(m_tolerance), but it seems a bit hacky.

\$\endgroup\$
1
\$\begingroup\$

I actually asked an (unfortunately unanswered :() question about something similar here Generic and accurate floating point "equality". Not trying to plug that - just wanted to give some reference.

While writing that code, I was relying pretty heavily on this article by Bruce Dawson. I strongly encourage reading it (and the series it is a part of). That being said, I think your code is fundamentally flawed as a generic solution. It might work as a specifically tailored solution, but you should

  • know a lot about its domain
  • thoroughly consider the consequences of your implementation
  • accept those consequences

If you try to use this as a generic solution it will likely always fail for certain classes of floating points, and can be very easily misused to fail for all floating point numbers.

While you shouldn't necessarily scrap your function, you should very clearly document the limitations of your implementation, and potentially document uses of the function if there is any ambiguity as to how accurate (or not) that comparison is likely to be.

  1. Using a threshold value doesn't always work very well, and requires the programmer to understand their domain and floating point.

What you're doing is basically bool isEqual = fabs(f1 – f2) <= epsilon;. There is no one-size fits all value for epsilon - the FLT_EPSILON in float.h is only reasonable for values between 1 and 2 - it becomes much too large/small very quickly as the numbers get smaller/bigger.

  1. Using ULPs doesn't help much (I have a personal preference for them, but as you'll see there is little to no difference)

If the integer representations of two same-sign floats are subtracted then the absolute value of the result is equal to one plus the number of representable floats between them.

This might lead you (as it did me) into thinking that you can use those to more effectively compare if something is equal (or if they're close enough that you can call it floating point error). The problem is, once again, scaling - an ULP is a huge difference for large numbers, and a tiny one for small ones. Using ULPs (or any relative comparison) is pretty reasonable for "normal" numbers.

As an aside - the relationships between ULPs and a relative difference (what you're doing) is pretty neat -

Ulps and FLT_EPSILON

Image credit: Bruce Dawson

  1. NaNs and infinities are weird, and there isn't a one-size fits all solution for them

Maybe you don't expect to ever find any NaNs or infinities in the code that uses this functor, or maybe you don't care. Either way, it should be documented that this may do something unexpected if a number is NaN/infinity. You could also choose how to handle this in your code - in many applications any kind of comparison with a NaN returns false, while you can generally do less than or greater compared to an infinity of the appropriate sign (if you're careful). Whatever you do, that choice should be documented.

  1. Zeroes and denormals will ruin your day

From Bruce Dawson:

It turns out that the entire idea of relative epsilons breaks down near zero. The reason is fairly straightforward. If you are expecting a result of zero then you are probably getting it by subtracting two numbers. In order to hit exactly zero the numbers you are subtracting need to be identical. If the numbers differ by one ULP then you will get an answer that is small compared to the numbers you are subtracting, but enormous compared to zero.

Small numbers break lots of things - hooray! Maybe this isn't an issue for your application - you deal with planets, not atoms - but it can really throw a wrench in your plans. In order to "reliably" compare to denormals and zero you need an absolute epsilon, not a relative one.


All in all - comparing floating point numbers generically and with confidence is hard. There are a few overall takeaways

  • When comparing against zero any type of relative comparison (epsilon or ULP based) is useless. You need an absolute epsilon, and finding the right value depends on your domain and will take tinkering (and probably still be wrong in some cases).
  • If you're comparing against non-zero (and more generally non-denormal) numbers then relative comparisons (either epsilon or ULP based) is fine. If there is a constant number being compared against then you could use an absolute epsilon.
  • Be mindful of how you handle NaNs and infinities - what does it mean for your application to encounter such values, and what does it mean to compare against them? This can only be handled by applying information about your domain to them.
  • From Bruce Dawson: > If you are comparing two arbitrary numbers that could be zero or non-zero then you need the kitchen sink. Good luck and God speed.

I don't have anything in particular to say about the code itself that wasn't mentioned by another answer, but I hope you consider the implications of floating point comparisons carefully.

\$\endgroup\$
0
\$\begingroup\$

As barak manos points out, your operator() body could be

return f1 < f2 && std::abs(f2 - f1) >= m_tolerance;

but, f1 < f2 is the same as 0 < f2 - f1, or f2 - f1 > 0. If that is true, then std::abs(f2 - f1) is just f2 - f1. So this first reduces to

return f2 - f1 > 0 && f2 - f1 >= m_tolerance;

Assuming that m_tolerance is non-negative, then if f2 - f1 >= m_tolerance, it follows that f2 - f1 must be > 0. So it further simplifies to

return f2 - f1 >= m_tolerance;

This seems to make intuitive sense. If f2 - f1 is not >= m_tolerance, then either f1 is outright larger than f2, or f1 is not smaller than f2 by enough to exceed the tolerance.

Oh, and I'd make the m_tolerance data member const.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.