Math_errhandling not working as expected with nvc++ compiler - part 2

Hello,

Following up on Math_errhandling not working as expected with nvc++ compiler.

With the same code snippet, turns out that whenever we have an optimization level bigger than 1, FE_INVALID is not set. i.e.:

O2

$ nvc++ -O2  t.cpp
"t.cpp", line 7: warning: unrecognized STDC pragma
  #pragma STDC FENV_ACCESS ON
               ^

$ ./a.out 
sqrt(100) = 10
sqrt(2) = 1.41421
golden ratio = 1.61803
sqrt(-0) = 0
errno_enabled: 1 check_fe_except: 2
sqrt(-1.0) = nan
    errno = 0 Success

O1

$ nvc++ -O1 t.cpp
"t.cpp", line 7: warning: unrecognized STDC pragma
  #pragma STDC FENV_ACCESS ON
               ^

 ./a.out 
sqrt(100) = 10
sqrt(2) = 1.41421
golden ratio = 1.61803
sqrt(-0) = 0
errno_enabled: 1 check_fe_except: 2
sqrt(-1.0) = -nan
    errno = 0 Success
    FE_INVALID raised

Whereas nvcc performs as expected:

$ nvcc -O2 t.cpp
$ ./a.out 
sqrt(100) = 10
sqrt(2) = 1.41421
golden ratio = 1.61803
sqrt(-0) = -0
errno_enabled: 1 check_fe_except: 2
sqrt(-1.0) = -nan
    errno = 33 Numerical argument out of domain
    errno = EDOM Numerical argument out of domain
    FE_INVALID raised

Is there a way to make use of the NVHPC math builtins (i.e. vsqrt) in order to have consistent FE Exceptions set regardless of the optimization level?

Thanks and regards,
Alex

What’s happening here is that at high opt level, the compiler sees that “sqrt(-1)” is invariant so is precomputing it at compile time. Hence vsqrt isn’t called at runtime, and no exception raised.

You’ll need to change the code so the compiler can’t do this optimization, for example by passing in “-1” as a command line argument:

% cat test.cpp
#include <iostream>
#include <cmath>
#include <cerrno>
#include <cfenv>
#include <cstring>

#pragma STDC FENV_ACCESS ON

int main( int argc, char *argv[] )
{
    int val;
    val = atoi(argv[1]);
    // normal use
    std::cout << "sqrt(100) = " << std::sqrt(100) << '\n'
              << "sqrt(2) = " << std::sqrt(2) << '\n'
              << "golden ratio = " << (1+std::sqrt(5))/2 << '\n';
    // special values
    std::cout << "sqrt(-0) = " << std::sqrt(-0.0) << '\n';
    // error handling
    errno = 0;
    const auto errno_enabled = math_errhandling & MATH_ERRNO;
    const auto check_fe_except = math_errhandling & MATH_ERREXCEPT;
    std::cout << "errno_enabled: " << errno_enabled << " check_fe_except: " << check_fe_except << std::endl;
    std::feclearexcept(FE_ALL_EXCEPT);
    double d = std::sqrt(val);;
    std::cout << "sqrt(" << val << ") = " << d << '\n';
    std::cout << "    errno = " << errno << " " << std::strerror(errno) << '\n';
    if(errno == EDOM)
        std::cout << "    errno = EDOM " << std::strerror(errno) << '\n';
    if(std::fetestexcept(FE_INVALID))
        std::cout << "    FE_INVALID raised\n";
}
% nvc++ -w -O2 test.cpp ; a.out -1
sqrt(100) = 10
sqrt(2) = 1.41421
golden ratio = 1.61803
sqrt(-0) = 0
errno_enabled: 1 check_fe_except: 2
sqrt(-1) = -nan
    errno = 0 Success
    FE_INVALID raised

-Mat

Thank you Mat,

Actually the described behavior is happening at runtime (I’ve tried to narrow it down to the example in the original thread but apparently there is something different in light of your reply).

In the original code, if we add a print before the FE exception check, it works as expected and we get FE_INVALID raised. You can check it out here:
https://github.com/neuronsimulator/nrn/blob/feexc-nvhpc/src/oc/math.cpp#L124

Thanks and regards,
Alex

Thanks Alex. I derived a test code from this (below), and as best I can tell, the problem is with the call to “std::feclearexcept”. If I simply don’t call this (or move it into main), then the invalid exception is raised.

You can also use the “-Mnobuiltin” so the libm sqrt is called and errno will be set.

% cat test.cpp
#include <iostream>
#include <cmath>
#include <cerrno>
#include <cfenv>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#define MAXERRCOUNT 5
#ifdef NOERRNO
const auto errno_enabled = false;
const auto check_fe_except = true;
#else
const auto errno_enabled = true;
const auto check_fe_except = false;
#endif
int hoc_errno_count;

#ifndef NO_CLEAR
static inline void clear_fe_except() {
    if (check_fe_except) {
        std::feclearexcept((0x20 | 0x04 | 0x10 | 0x08 | 0x01));
    }
}
#endif

double hoc_errcheck(double d, const char* s) /* check result of library call */
{

    if ((errno_enabled && errno == EDOM) || (check_fe_except && std::fetestexcept(FE_INVALID))) {
        if (check_fe_except) {
            std::feclearexcept(FE_ALL_EXCEPT);
        }
        errno = 0;
        fprintf(stderr, "argument out of domain\n");
    } else if ((errno_enabled && errno == ERANGE) ||
               (check_fe_except &&
                (std::fetestexcept(FE_DIVBYZERO) || std::fetestexcept(FE_OVERFLOW) ||
                 std::fetestexcept(FE_UNDERFLOW)))) {
        if (check_fe_except) {
            std::feclearexcept(FE_ALL_EXCEPT);
        }
        errno = 0;
        if (++hoc_errno_count > MAXERRCOUNT) {
        } else {
            fprintf(stderr, "result out of range\n");
            if (hoc_errno_count == MAXERRCOUNT) {
                fprintf(stderr, "No more errno warnings during this execution\n");
            }
        }
    }
    return d;
}

double hoc_Sqrt(double x) {
#ifndef NO_CLEAR
    clear_fe_except();
#endif
    return hoc_errcheck(sqrt(x), "sqrt");
}

int main() {

   double res;
   int i;
   for (i=10; i>=-1;--i) {
        res = hoc_Sqrt(i);
        printf("RES %d: %f\n",i,res);
   }

}
% nvc++ -O2 -Mnobuiltin test.cpp ; a.out
RES 10: 3.162278
RES 9: 3.000000
RES 8: 2.828427
RES 7: 2.645751
RES 6: 2.449490
RES 5: 2.236068
RES 4: 2.000000
RES 3: 1.732051
RES 2: 1.414214
RES 1: 1.000000
RES 0: 0.000000
argument out of domain
RES -1: -nan
% nvc++ -O2 -DNOERRNO -Mnobuiltin test.cpp ; a.out
RES 10: 3.162278
RES 9: 3.000000
RES 8: 2.828427
RES 7: 2.645751
RES 6: 2.449490
RES 5: 2.236068
RES 4: 2.000000
RES 3: 1.732051
RES 2: 1.414214
RES 1: 1.000000
RES 0: 0.000000
argument out of domain
RES -1: -nan
% nvc++ -O2 -DNOERRNO  test.cpp ; a.out
RES 10: 3.162278
RES 9: 3.000000
RES 8: 2.828427
RES 7: 2.645751
RES 6: 2.449490
RES 5: 2.236068
RES 4: 2.000000
RES 3: 1.732051
RES 2: 1.414214
RES 1: 1.000000
RES 0: 0.000000
RES -1: -nan
% nvc++ -O2 -DNOERRNO -DNO_CLEAR test.cpp ; a.out
RES 10: 3.162278
RES 9: 3.000000
RES 8: 2.828427
RES 7: 2.645751
RES 6: 2.449490
RES 5: 2.236068
RES 4: 2.000000
RES 3: 1.732051
RES 2: 1.414214
RES 1: 1.000000
RES 0: 0.000000
argument out of domain
RES -1: -nan

Thanks Mat for looking into this. We will experiment further.

Hello Mat,

Given your suggestions we have fixed the issue for NVHPC 22.3 by compiling the offending file with something similar to -O1 -DNOERRNO.
All worked well until we tried using NVHPC 23.1 where we see an underflow exception when calling pow(0, 2).
Here is an updated version of the above code snippet that executes the pow and sqrt

functions:
#include <iostream>
#include <cmath>
#include <cerrno>
#include <cfenv>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#define MAXERRCOUNT 5
#ifdef NOERRNO
const auto errno_enabled = false;
const auto check_fe_except = true;
#else
const auto errno_enabled = true;
const auto check_fe_except = false;
#endif
int hoc_errno_count;

#ifndef NO_CLEAR
static inline void clear_fe_except() {
    if (check_fe_except) {
        std::feclearexcept((0x20 | 0x04 | 0x10 | 0x08 | 0x01));
    }
}
#endif

double hoc_errcheck(double d, const char* s) /* check result of library call */
{

    if ((errno_enabled && errno == EDOM) || (check_fe_except && std::fetestexcept(FE_INVALID))) {
        if (check_fe_except) {
            std::feclearexcept(FE_ALL_EXCEPT);
        }
        errno = 0;
        fprintf(stderr, "argument out of domain\n");
    } else if ((errno_enabled && errno == ERANGE) ||
               (check_fe_except &&
                (std::fetestexcept(FE_DIVBYZERO) || std::fetestexcept(FE_OVERFLOW) ||
                 std::fetestexcept(FE_UNDERFLOW)))) {
        if (std::fetestexcept(FE_DIVBYZERO)) {
            std::cout << "divbyzero" << std::endl;
        }
        if (std::fetestexcept(FE_OVERFLOW)) {
            std::cout << "overflow" << std::endl;
        }
        if (std::fetestexcept(FE_UNDERFLOW)) {
            std::cout << "underflow" << std::endl;
        }
        if (check_fe_except) {
            std::feclearexcept(FE_ALL_EXCEPT);
        }
        errno = 0;
        if (++hoc_errno_count > MAXERRCOUNT) {
        } else {
            fprintf(stderr, "%s result out of range\n", s);
            if (hoc_errno_count == MAXERRCOUNT) {
                fprintf(stderr, "No more errno warnings during this execution\n");
            }
        }
    }
    return d;
}

double hoc_Sqrt(double x) {
#ifndef NO_CLEAR
    clear_fe_except();
#endif
    return hoc_errcheck(sqrt(x), "sqrt");
}

double hoc_Pow(double x, double y) {
#ifndef NO_CLEAR
    clear_fe_except();
#endif
    return hoc_errcheck(pow(x, y), "exponentiation");
}

int main() {

   double res;
   int i;
   for (i=1; i>=-1;--i) {
        res = hoc_Pow(i,2);
        printf("RES pow %d: %f\n",i,res);
   }
   for (i=1; i>=-1;--i) {
        res = hoc_Sqrt(i);
        printf("RES sqrt %d: %f\n",i,res);
   }

}

Here is also the output from the compilation and execution:

$ nvc++ -O1 -DNOERRNO test.cpp; ./a.out
RES pow 1: 1.000000
underflow
exponentiation result out of range
RES pow 0: 0.000000
RES pow -1: 1.000000
RES sqrt 1: 1.000000
RES sqrt 0: 0.000000
argument out of domain
RES sqrt -1: -nan

Give this output we believe that there is some issue with the implementation of the pow function by the newest NVHPC.
Do you think there is an easy fix around that, apart from using -Mnobuiltin or completely disabling exceptions?

I thin the solution here is to add the “-Kieee” flag. Without it, we don’t guarantee adherence to IEEE 754 nor about setting ERRNO, or properly managing overflow/underflow.

Performance of the application may suffer a bit, Then again, you are compiling with -O1 so the compiler doesn’t optimize away the pow calls, so performance may not be of interest here.

% nvc++ -O1 -DNOERRNO -Kieee test.cpp ; ./a.out
RES pow 1: 1.000000
RES pow 0: 0.000000
RES pow -1: 1.000000
RES sqrt 1: 1.000000
RES sqrt 0: 0.000000
argument out of domain
RES sqrt -1: -nan

Hello Mat,

Thank you very much for your reply.
Indeed we don’t care that much for the performance in these specific calls so we can get away with -Kieee which generates the proper exceptions.

Ioannis