给定以下GCC 7.3.1中非常简单的程序:
//exponentTest.cc
#include <complex>
#include <iostream>
int main(int argc, char **argv)
{
std::complex<double> iCpx = std::complex<double>(0,1);
double baseline[3] = {-0.1, 0, 0};
double theta = atan2(baseline[1], baseline[0]);
std::cout << "Theta: " << theta << std::endl;
for(size_t m =1; m < 10; m++)
{
std::complex<double> thetExp = exp(-1 * m * theta * iCpx);
std::cout << "m(" << m << "): " << thetExp << std::endl;
}
return 0;
}
当θ为PI时,这段代码给出了一个不正确的结果:
[stix@localhost ~]$ gcc exponentTest.cc -o expTest -lm -lstdc++
[stix@localhost ~]$ ./expTest
Theta: 3.14159
m(1): (-0.963907,0.26624)
m(2): (-0.963907,0.26624)
m(3): (-0.963907,0.26624)
m(4): (-0.963907,0.26624)
m(5): (-0.963907,0.26624)
m(6): (-0.963907,0.26624)
m(7): (-0.963907,0.26624)
m(8): (-0.963907,0.26624)
m(9): (-0.963907,0.26624)
正确答案应该是+-1交替。
在一阵哀号和咬牙切齿之后,我把问题归结为在for()循环中使用了size_t。我用一个无符号int替换了它,单元测试工作了,但是我更广泛的代码库中使用了它。沮丧的是,我最终显式地将std::exp的输入强制转换为double:
//Improved exponentTest.cc
#include <complex>
#include <iostream>
int main(int argc, char **argv)
{
std::complex<double> iCpx = std::complex<double>(0,1);
double baseline[3] = {-0.1, 0, 0};
double theta = atan2(baseline[1], baseline[0]);
std::cout << "Theta: " << theta << std::endl;
for(size_t m = 1; m < 10; m++)
{
std::complex<double> thetExp = exp(-1 * (double)m * theta * iCpx);
std::cout << "m(" << m << "): " << thetExp << std::endl;
}
return 0;
}
这个版本始终给出正确的结果:
[stix@localhost ~]$ gcc exponentTest.cc -o expTest -lm -lstdc++
[stix@localhost ~]$ ./expTest
Theta: 3.14159
m(1): (-1,-1.22465e-16)
m(2): (1,2.44929e-16)
m(3): (-1,-3.67394e-16)
m(4): (1,4.89859e-16)
m(5): (-1,-6.12323e-16)
m(6): (1,7.34788e-16)
m(7): (-1,-8.57253e-16)
m(8): (1,9.79717e-16)
m(9): (-1,-1.10218e-15)
问题解决了。但是,我不知道为什么会出现这个问题,而且它有点像编译器错误。最糟糕的是,如果没有在exp()函数中显式强制转换m,问题就不一致;有时它能提供正确的结果,有时却不能。
我的理解是c++标准要求编译器自动将所有int型转换为double型,例如:
double = int * double * int * double;
但是编译器显然没有这样做。
这是一个编译器的错误,或者有一些gotcha我没有考虑在混合双精度和整型?
编辑:每个注释中的指数行应该抛出一个警告,因为unsigned类型被乘以-1,然而,情况并非如此:
[stix@localhost ~]$ gcc exponentTest.cc -o expTest -lm -lstdc++ -Wall -Wextra -pedantic-errors
exponentTest.cc: In function ‘int main(int, char**)’:
exponentTest.cc:6:14: warning: unused parameter ‘argc’ [-Wunused-parameter]
int main(int argc, char **argv)
^~~~
exponentTest.cc:6:27: warning: unused parameter ‘argv’ [-Wunused-parameter]
int main(int argc, char **argv)
^~~~
相关软件版本(GCC 7使用devtoolset-7):
[stix@localhost ~]$ gcc --version
gcc (GCC) 7.3.1 20180303 (Red Hat 7.3.1-5)
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
[stix@localhost ~]$ cat /etc/redhat-release
CentOS Linux release 7.9.2009 (Core)
[stix@localhost ~]$ rpm -qa | grep devtoolset
devtoolset-7-gcc-plugin-devel-7.3.1-5.16.el7.x86_64
devtoolset-7-binutils-2.28-11.el7.x86_64
devtoolset-7-gcc-7.3.1-5.16.el7.x86_64
devtoolset-7-gcc-gfortran-7.3.1-5.16.el7.x86_64
devtoolset-7-runtime-7.1-4.el7.x86_64
devtoolset-7-libquadmath-devel-7.3.1-5.16.el7.x86_64
devtoolset-7-gcc-gdb-plugin-7.3.1-5.16.el7.x86_64
devtoolset-7-libstdc++-devel-7.3.1-5.16.el7.x86_64
devtoolset-7-gcc-c++-7.3.1-5.16.el7.x86_64
最简单的修复方法是将1
更改为1.0
。这就迫使计算必须使用double
s:
std::complex<double> thetExp = exp(-1.0 * m * theta * iCpx);
为什么?让我们看一下失败的表达式:
-1 * m * theta * iCpx
这里的类型是:
int * size_t * double * std::complex<double>
c++不会查看所有类型并选择"最高的"。要促进一切。相反,它从左到右逐个查看二进制操作,就好像有括号将它们分组,如:
(((int * size_t) * double) * std::complex<double>)
您会遇到麻烦,因为int * size_t
首先执行。整型提升规则适用,int
被转换为size_t
,因为在您的平台上size_t
更大。这意味着一个32位带符号整数被转换为一个64位无符号整数。
试试这个,你可以看到问题:
std::cout << (size_t) -1 << "n";
它打印:
18446744073709551615
当您将-1 * m
更改为-1 * (double) m
时,消除了有符号/无符号问题。-1
被提升为double
,即-1.0
。
乘法运算符(*
)具有从左到右的结合性。因此,在你的参数表达式中:
exp(-1 * m * theta * iCpx)
-1 * m
首先执行,并使用size_t
类型完成。这将导致very(由于-1
被解释为无符号常量),当转换/强制转换为double
时,几乎肯定会失去精度。
在你的循环中添加一些诊断可以揭示问题的本质:
for (size_t m = 1; m < 10; m++) {
std::cout << (-1 * m) << " " << (-1 * (double)m) << std::endl; // Diagnostic!
std::complex<double> thetExp = exp(-1 * m * theta * iCpx);
std::cout << "m(" << m << "): " << thetExp << std::endl;
}
输出:
Theta: 3.14159
18446744073709551615 -1
m(1): (-0.963907,0.26624)
18446744073709551614 -2
m(2): (-0.963907,0.26624)
18446744073709551613 -3
m(3): (-0.963907,0.26624)
18446744073709551612 -4
m(4): (-0.963907,0.26624)
18446744073709551611 -5
m(5): (-0.963907,0.26624)
18446744073709551610 -6
m(6): (-0.963907,0.26624)
18446744073709551609 -7
m(7): (-0.963907,0.26624)
18446744073709551608 -8
m(8): (-0.963907,0.26624)
18446744073709551607 -9
m(9): (-0.963907,0.26624)
显式地将m
转换为double
强制将初始乘法(正确地)执行为双精度算术。(注意:将m
转换为(带符号的)int
也可以。)
注意clang-cl会发出警告:
警告:隐式转换将符号:'int'更改为'unsigned '[-Wsign-conversion]