使用 quiet_NaN() 和 -Ofast 时检测不到 NaN
Undetectable NaN when using quiet_NaN() and -Ofast
我正在用 OpenCV 编写一个经典的 nanmean
函数。
我尝试通过默认行为模拟 MatLab 的 nanmean
(即 nanmean
在第一维上减少)。
我生成了一个随机大小的矩阵,可以是 CV_32F
或 CV_64F
,最多有 4 个通道。我按照统一的法则用随机值填充它。
然后我使用 std::numerical_limits<float>
为 Nan 分配一些值(如果 CV_32F
,否则 double
) :: quiet_NaN()
;
在调试步骤中,我正在寻找问题并打印以下内容:
T v = *it_src;
std::cout<<"v before: "<<v<<" "<<std::isnan(v)<<" "<<cvIsNaN(v)<<" "<<(v==v)<<" "<<" "<<std::isinf(v)<<" "<<((v+v)==v)<<std::endl;
T是模板类型,可以是float
或者double
,没有别的。
输出为:
v before: nan 0 0 1 0 1
因此值为“nan”,但“std::isnan”和“CvIsNan”都无法检测到它。
IEEE 754 的比较功能(如果 v 是 Nan,则 v == v
应该为假)失败(v == v
return true
)。唯一有效的是最后一次检查 ((v+v) == v
)。
我有几个问题:
- 为什么?
- 这个问题是从哪里来的,如何解决?
- SIMD 指令是否也受此影响?
#include <opencv2/core.hpp>
#include <iostream>
using namespace cv;
int main()
{
// Initialization
int rows(theRNG().uniform(10,21)), cols(theRNG().uniform(10,21));
int cn(theRNG().uniform(1, 5)); // [1, 5[ -> [1,4]
Mat src(rows, cols, CV_32FC(cn));
theRNG().fill(src, RNG::UNIFORM, 0, 10000);
float ratio = theRNG().uniform(0.1f, 0.8f);
int nb_points = saturate_cast<int>(src.total() * ratio);
for(int i=0;i<nb_points;i++)
{
int x = theRNG().uniform(0, cols);
int y = theRNG().uniform(0, rows);
int z = theRNG().uniform(0, cn);
src.ptr<float>(y,x)[z] = std::numeric_limits<float>::quiet_NaN();
}
Mat dst = Mat::zeros(1, cols, src.type());
// Computation (reduce mean with omition of the Nan value over the first axis).
const size_t src_step1 = src.step1();
for(int c=0; c<cols; c++)
{
// The default constructor of Scalar_ initialize the elements of the attribute "val" to 0.
Scalar sum;
Scalar_<int> cnt;
const float* it_src = src.ptr<float>(0,c);
for(int r=0; r<rows; r++, it_src+=src_step1)
for(int i=0;i<cn;i++)
{
float v = it_src[i];
std::cout<<"v before: "<<v<<" "<<std::isnan(v)<<" "<<cvIsNaN(v)<<" "<<(v==v)<<" "<<" "<<std::isinf(v)<<" "<<((v+v)==v)<<std::endl;
if(!std::isnan(v)) // Failing
{
sum[i]+= saturate_cast<double>(v);
cnt[i]++;
}
}
for(int i=0; i<cn;i++)
{
float den = saturate_cast<float>(cnt[i]);
if(den==0.f)
den = 1.f;
dst.ptr<float>(0, c)[i] = saturate_cast<float>(sum[i]) / den;
}
}
return 0;
}
您在评论中提到您正在使用 -Ofast
,这就是导致问题的原因。要理解这是为什么,我们首先要查看 GCC documentation for options that control optimizations。此处列出了 -Ofast
打开的以下选项:
It turns on -ffast-math, -fallow-store-data-races and the
Fortran-specific -fstack-arrays, unless -fmax-stack-var-size is
specified, and -fno-protect-parens.
对于您的情况,查看 -ffast-math
的描述似乎是相关的,因为它设置了以下选项集:
Sets the options -fno-math-errno, -funsafe-math-optimizations,
-ffinite-math-only, -fno-rounding-math, -fno-signaling-nans, -fcx-limited-range and -fexcess-precision=fast.
我没有查看所有这些选项的作用,但 -ffinite-math-only
具体看来它与任何关心 NaN 或 Inf 的程序都不兼容,因为它被记录为:
Allow optimizations for floating-point arithmetic that assume that
arguments and results are not NaNs or +-Infs.
This option is not turned on by any -O option since it can result in
incorrect output for programs that depend on an exact implementation
of IEEE or ISO rules/specifications for math functions. It may,
however, yield faster code for programs that do not require the
guarantees of these specifications. [emphasis added]
由于您的用例明确包括检测 NaN,因此似乎ill-advised 使用此标志。
使用 -Ofast
选项 (Example on Godbolt because I don't have access to GCC right now) 使用 GCC 11.3 编译时演示问题的简单示例程序。使用 -O3
时,不会按预期打印任何内容,而使用 -Ofast
时,它会打印 Brought to you by -Ofast
:
#include <limits>
#include <cstdio>
#include <cmath>
int main() {
auto value = std::numeric_limits<float>::quiet_NaN();
if(!std::isnan(value)) {
std::puts("Brought to you by -Ofast");
}
}
我正在用 OpenCV 编写一个经典的 nanmean
函数。
我尝试通过默认行为模拟 MatLab 的 nanmean
(即 nanmean
在第一维上减少)。
我生成了一个随机大小的矩阵,可以是 CV_32F
或 CV_64F
,最多有 4 个通道。我按照统一的法则用随机值填充它。
然后我使用 std::numerical_limits<float>
为 Nan 分配一些值(如果 CV_32F
,否则 double
) :: quiet_NaN()
;
在调试步骤中,我正在寻找问题并打印以下内容:
T v = *it_src;
std::cout<<"v before: "<<v<<" "<<std::isnan(v)<<" "<<cvIsNaN(v)<<" "<<(v==v)<<" "<<" "<<std::isinf(v)<<" "<<((v+v)==v)<<std::endl;
T是模板类型,可以是float
或者double
,没有别的。
输出为:
v before: nan 0 0 1 0 1
因此值为“nan”,但“std::isnan”和“CvIsNan”都无法检测到它。
IEEE 754 的比较功能(如果 v 是 Nan,则 v == v
应该为假)失败(v == v
return true
)。唯一有效的是最后一次检查 ((v+v) == v
)。
我有几个问题:
- 为什么?
- 这个问题是从哪里来的,如何解决?
- SIMD 指令是否也受此影响?
#include <opencv2/core.hpp>
#include <iostream>
using namespace cv;
int main()
{
// Initialization
int rows(theRNG().uniform(10,21)), cols(theRNG().uniform(10,21));
int cn(theRNG().uniform(1, 5)); // [1, 5[ -> [1,4]
Mat src(rows, cols, CV_32FC(cn));
theRNG().fill(src, RNG::UNIFORM, 0, 10000);
float ratio = theRNG().uniform(0.1f, 0.8f);
int nb_points = saturate_cast<int>(src.total() * ratio);
for(int i=0;i<nb_points;i++)
{
int x = theRNG().uniform(0, cols);
int y = theRNG().uniform(0, rows);
int z = theRNG().uniform(0, cn);
src.ptr<float>(y,x)[z] = std::numeric_limits<float>::quiet_NaN();
}
Mat dst = Mat::zeros(1, cols, src.type());
// Computation (reduce mean with omition of the Nan value over the first axis).
const size_t src_step1 = src.step1();
for(int c=0; c<cols; c++)
{
// The default constructor of Scalar_ initialize the elements of the attribute "val" to 0.
Scalar sum;
Scalar_<int> cnt;
const float* it_src = src.ptr<float>(0,c);
for(int r=0; r<rows; r++, it_src+=src_step1)
for(int i=0;i<cn;i++)
{
float v = it_src[i];
std::cout<<"v before: "<<v<<" "<<std::isnan(v)<<" "<<cvIsNaN(v)<<" "<<(v==v)<<" "<<" "<<std::isinf(v)<<" "<<((v+v)==v)<<std::endl;
if(!std::isnan(v)) // Failing
{
sum[i]+= saturate_cast<double>(v);
cnt[i]++;
}
}
for(int i=0; i<cn;i++)
{
float den = saturate_cast<float>(cnt[i]);
if(den==0.f)
den = 1.f;
dst.ptr<float>(0, c)[i] = saturate_cast<float>(sum[i]) / den;
}
}
return 0;
}
您在评论中提到您正在使用 -Ofast
,这就是导致问题的原因。要理解这是为什么,我们首先要查看 GCC documentation for options that control optimizations。此处列出了 -Ofast
打开的以下选项:
It turns on -ffast-math, -fallow-store-data-races and the Fortran-specific -fstack-arrays, unless -fmax-stack-var-size is specified, and -fno-protect-parens.
对于您的情况,查看 -ffast-math
的描述似乎是相关的,因为它设置了以下选项集:
Sets the options -fno-math-errno, -funsafe-math-optimizations, -ffinite-math-only, -fno-rounding-math, -fno-signaling-nans, -fcx-limited-range and -fexcess-precision=fast.
我没有查看所有这些选项的作用,但 -ffinite-math-only
具体看来它与任何关心 NaN 或 Inf 的程序都不兼容,因为它被记录为:
Allow optimizations for floating-point arithmetic that assume that arguments and results are not NaNs or +-Infs.
This option is not turned on by any -O option since it can result in incorrect output for programs that depend on an exact implementation of IEEE or ISO rules/specifications for math functions. It may, however, yield faster code for programs that do not require the guarantees of these specifications. [emphasis added]
由于您的用例明确包括检测 NaN,因此似乎ill-advised 使用此标志。
使用 -Ofast
选项 (Example on Godbolt because I don't have access to GCC right now) 使用 GCC 11.3 编译时演示问题的简单示例程序。使用 -O3
时,不会按预期打印任何内容,而使用 -Ofast
时,它会打印 Brought to you by -Ofast
:
#include <limits>
#include <cstdio>
#include <cmath>
int main() {
auto value = std::numeric_limits<float>::quiet_NaN();
if(!std::isnan(value)) {
std::puts("Brought to you by -Ofast");
}
}