Search code examples
gccg++c++17sfml

Different output with different optimization levels in GCC


I am working on a rewrite of a raytracer that I developed for university last semester and I am running into the following problem: When I compile and run my code in Debug the output is as expected

expected result

But when I enable higher optimization levels e.g. "-O2" something completely different is the result:

actual result

And I am not sure why this happens. I tracked it down to the sphere intersection code

//#pragma GCC push_options
//#pragma GCC optimize("O0")

Intersection Sphere::intersect(const Ray& ray, const float previous) const
{
    const auto oc = ray.origin - center_;
    const auto lhv = -dot(ray.direction, oc);
    const auto discriminant = lhv * lhv - (oc.lensqr() - radius_ * radius_);

    if (discriminant < 0.0F)
    {
        return Intersection::failure();
    }
    float distance;
    const auto rhv = std::sqrt(discriminant);
    const auto r = std::minmax(lhv + rhv, lhv - rhv);
    if (r.first <= 0.0F)
    {
        if (r.second <= 0.0F)
        {
            return Intersection::failure();
        }
        distance = r.second;
    }
    else
    {
        distance = r.first;
    }

    const auto hit = ray.getPoint(distance);
    const auto normal = (hit - center_).normalize();

    if (0.0F <= distance && distance < previous - epsilon)
    {
        return {distance, ray, this, normal, hit};
    }
    return Intersection::failure();
}

//#pragma GCC pop_options

If I uncomment the pragma in release mode, I get the expected result again. Maybe I have some undefined behaviour in my code that leads to this?

You can also have a look here as a minimal reproducible example is not easily possible. https://github.com/Yamahari/RayTracer/blob/master/rt/solid/Sphere.cpp

(You can also clone the repo and build the project with cmake, you only need SFML as a dependency.

Use -DSFML_INCLUDE_DIR="include_dir" and -DSFML_LIB_DIR="lib_dir" with the sfml library compiled with your desired compiler)


Solution

  • std::minmax returns a pair of references to its arguments. If you call it with prvalues as arguments, the references in this pair will be dangling after the end of the full-expression, which means that the access r.first in

    if (r.first <= 0.0F)
    

    will have undefined behavior here.

    So store lhv + rhv and lhv - rhv in variables and call std::minmax on them or use

    std::minmax({lhv + rhv, lhv - rhv})
    

    to select the std::initializer_list overload, which returns a pair of actual values.


    As noted by @Jarod42 in the comments, you don't actually need std::minmax here. rhv is the result of a std::sqrt call, so it is always non-negative, making lhv + rhv always the bigger (or equal) value.