Why does Release/Debug have a different result for std::min?
Asked Answered
F

3

42

Here is the test program:

void testFunc()
{
    double maxValue = DBL_MAX;
    double slope = std::numeric_limits<double>::quiet_NaN();

    std::cout << "slope is " << slope << std::endl;
    std::cout << "maxThreshold is " << maxValue << std::endl;
    std::cout << "the_min is " << std::min( slope, maxValue) << std::endl;
    std::cout << "the_min is " << std::min( DBL_MAX, std::numeric_limits<double>::quiet_NaN()) << std::endl;
}

int main( int argc, char* argv[] )
{
    testFunc();
    return 0;
}

In Debug, I get:

slope is nan
maxThreshold is 1.79769e+308
the_min is nan
the_min is 1.79769e+308

In Release, I get:

slope is nan
maxThreshold is 1.79769e+308
the_min is 1.79769e+308
the_min is nan

Why would I get a different result in Release than Debug?

I already checked Stack Overflow post Use of min and max functions in C++, and it does not mention any Release/Debug differences.

I am using Visual Studio 2015.

Forelimb answered 7/10, 2016 at 13:58 Comment(7)
I think the problem is not with std::min, it's with std::numeric_limits<double>::quiet_NaN()Consternation
I remember the 64-bit version of cl having problems with correct NaN handling in Release mode, but that was in VS 2008. I would hope they fixed it since then.Larry
I am not sure NaN will fit in LessThanComparableKraken
I suppose both behaviors are valid because the actual representation of nan is implementation defined. You shouldn't ever compare against nan, even with another nan.Offensive
For GCC you can set -frounding-math -fsignaling-nans on the compiler so that it guarantees strict IEEE 754 compliance. That way it should never re-order comparisons (or any other optimisations) which might change behaviour due to NaN. For Visual Studio, /fp:precise appears to be the equivalent (which promises not to optimise things such as x-x=0).Deathful
@Dave: /fp:precise is enabled by default and this does not prevent Debug behaviour to be different than Release...Forelimb
In that case I can only assume it's a bug / missing feature in the implementation, since they mention that the option is explicitly to enable correct handling of NaN (my suspicion is that a compiler optimisation stage hasn't been properly flagged; maybe raise a bug?). I see that there is also /fp:strict, which may also be worth a shot (but potentially incurs a lot of overhead).Deathful
F
27

Got it:

Here is the implementation used by VS in Debug mode (with _Pred being DEBUG_LT, LT for Lower Than):

template<class _Pr,
    class _Ty1,
    class _Ty2> inline
    _CONST_FUN bool _Debug_lt_pred(_Pr _Pred,
        _Ty1&& _Left, _Ty2&& _Right,
        _Dbfile_t _File, _Dbline_t _Line)
    {   // test if _Pred(_Left, _Right) and _Pred is strict weak ordering
    return (!_Pred(_Left, _Right)
        ? false
        : _Pred(_Right, _Left)
            ? (_DEBUG_ERROR2("invalid comparator", _File, _Line), true)
            : true);
    }

Which is equivalent to (more readable):

    if (!_Pred(_Left, _Right))
    {
        return false;
    }
    else
    {
        if ( _Pred(_Right, _Left) )
        {
            assert( false );
            return true;
        }
        else
        {
            return true;
        }
    }

Which, again is equivalent to (!_Pred(_Left, _Right)). Transcripted as a macro, it becomes #define _DEBUG_LT(x, y) !((y) < (x)) (i.e: NOT right < left).

Release implementation is actually a macro #define _DEBUG_LT(x, y) ((x) < (y)) (i.e: left < right).

So Debug (!(y<x)) and Release (x<y) implementations are definitely not the same and they do behave differently if one parameter is a NaN...! Don't ask why they did that....

Forelimb answered 7/10, 2016 at 14:37 Comment(3)
Nice sleuthing.Blasphemous
Well, they did it to catch bugs. In debug builds, it evaluates the expression both ways, and asserts that they produce the same answer. In release builds, you assume that the app has been debugged and the comparators are correct, so you don't want to pay the penalty of doing two comparisons.Avernus
Sure, but at least, they could have make sure that both end up with the same result....that would have been possible by checking both ways in a different order.Forelimb
W
39

In IEEE 754 comparing NAN to anything will always yield false, no matter what it is.

slope > 0; // false
slope < 0; // false
slope == 0; // false

And, more importantly for you

slope < DBL_MAX; // false
DBL_MAX < slope; // false

So it seems that the compiler reorders the parameters/uses > or <= instead of <, and that's why you get the differing results.

For example, those functions could be described as such

Release:

double const& min(double const& l, double const r) {
    return l <= r ? l : r;
}

Debug:

double const& min(double const& l, double const& r) {
    return r < l ? r : l;
}

The requirements (LessThanComparable) on std::min aside, those have the same meaning arithmetically. But they yield different results when you use them with NaN.

Woody answered 7/10, 2016 at 14:6 Comment(7)
An additional point to whoever might be interested: In IEEE754 comparing NAN to anything including itself will always yield false. BTW, this SO question was well-posed and has an interesting set of answers - including several quite amusing catfights in the comments.Bellamy
I was about to comment that "anything" includes false, but when I checked NaN ∈ anything I got false.Woody
@Woody Try checking NaN ∉ anything instead.Smasher
@Woody shouldn't it rather swap the r : l than the r < l, since otherwise it still evaluates to false and l is returned? I.e. r < l ? r : l; -> r > l ? l : r;Periodicity
Something is definitely wrong. I'll have to wrap my head around this, because it also has to fit Remarks: Returns the first argument when the arguments are equivalent. both times.Woody
@Periodicity would you mind looking at it now? I think it's correct and true to the example now.Woody
Looks better, yes.Periodicity
F
27

Got it:

Here is the implementation used by VS in Debug mode (with _Pred being DEBUG_LT, LT for Lower Than):

template<class _Pr,
    class _Ty1,
    class _Ty2> inline
    _CONST_FUN bool _Debug_lt_pred(_Pr _Pred,
        _Ty1&& _Left, _Ty2&& _Right,
        _Dbfile_t _File, _Dbline_t _Line)
    {   // test if _Pred(_Left, _Right) and _Pred is strict weak ordering
    return (!_Pred(_Left, _Right)
        ? false
        : _Pred(_Right, _Left)
            ? (_DEBUG_ERROR2("invalid comparator", _File, _Line), true)
            : true);
    }

Which is equivalent to (more readable):

    if (!_Pred(_Left, _Right))
    {
        return false;
    }
    else
    {
        if ( _Pred(_Right, _Left) )
        {
            assert( false );
            return true;
        }
        else
        {
            return true;
        }
    }

Which, again is equivalent to (!_Pred(_Left, _Right)). Transcripted as a macro, it becomes #define _DEBUG_LT(x, y) !((y) < (x)) (i.e: NOT right < left).

Release implementation is actually a macro #define _DEBUG_LT(x, y) ((x) < (y)) (i.e: left < right).

So Debug (!(y<x)) and Release (x<y) implementations are definitely not the same and they do behave differently if one parameter is a NaN...! Don't ask why they did that....

Forelimb answered 7/10, 2016 at 14:37 Comment(3)
Nice sleuthing.Blasphemous
Well, they did it to catch bugs. In debug builds, it evaluates the expression both ways, and asserts that they produce the same answer. In release builds, you assume that the app has been debugged and the comparators are correct, so you don't want to pay the penalty of doing two comparisons.Avernus
Sure, but at least, they could have make sure that both end up with the same result....that would have been possible by checking both ways in a different order.Forelimb
D
23

You didn't specify which floating point representation format your processor uses. But, since you use Visual Studio, I'll assume that you use Windows, and then I'll assume that your processor uses IEEE 754 representation.

In IEEE 754, NaN is unordered in respect to every number. This means that (NaN < f) == false and (f < NaN) == false for any value of f. Pedantically, this means that floating point numbers that support NaN do not meet the requirements of LessThanComparable which is a requirement for std::min. Practically std::min behaves as specified in the standard as long as neither argument is NaN.

Since one of the arguments is NaN in your code, the result is unspecified by the standard - it could be one or the other depending on any external factors such as release vs debug build, version of compiler, phase of the moon, etc.

Duer answered 7/10, 2016 at 14:18 Comment(3)
It looks to me like floating-point numbers satisfy all requirements of LessThanComparable. LessThanComparable doesn't require a total order; it only requires a strict weak ordering. The fact that any comparison involving NaN returns false doesn't violate any requirements. (This is all ignoring signalling NaNs, any use of which is UB.)Hypervitaminosis
@Hypervitaminosis upon further inspection of the rules, I think that you are indeed correct. I shall fix the answer.Duer
Wait, what the - actually, it looks like the cppreference page is wrong, and it's been missing one of the strict weak ordering requirements for years: defining equiv(a, b) as !(a < b) && !(b < a), it is required that equiv(a, b) && equiv(b, c) implies equiv(a, c). NaN violates that requirement, so it looks like floating-point numbers really don't satisfy LessThanComparable.Hypervitaminosis

© 2022 - 2024 — McMap. All rights reserved.