我使用的是MinGW-w 64 g编译器10.2和10.3。
Windows上的g有一个奇怪的地方:应用程序的主线程将以双精度执行浮点运算,但其他线程将以扩展精度执行浮点运算。这可以用这个小程序(“Test.cpp”)重现:
#include <iostream>
#include <future>
#include <cmath>
#include <string>
#include <thread>
std::string getResult()
{
std::cout << "Thread id: " << std::this_thread::get_id() << std::endl;
std::string result = "Result:\n";
double a, b, c;
int i;
for (i = 0, a = 1.0; i < 10000000; i++)
{
a *= 1.00000001;
b = sqrt(a);
c = pow(a, 0.5);
if (std::abs(b - c) < 1.0e-50)
continue;
result += std::string("a: ") + std::to_string(a) + " - Numbers differ.\n";
}
return result;
}
int main()
{
std::string string1 = getResult();
std::future<std::string> result = std::async(std::launch::async, getResult);
std::string string2 = result.get();
if (string1 != string2)
{
std::cout << "The results are different." << std::endl;
}
else
{
std::cout << "The results are the same." << std::endl;
}
return 0;
}
当使用optimization(!)编译它时,如下所示:g++ -o Test.exe Test.cpp -O2
并执行它,输出为:
C:\...Path...>Test.exe
Thread id: 1
Thread id: 2
The results are different.
对我来说这是一个主要的问题。出于安全原因,我希望所有的数值结果总是相同的-无论它们是在不同的线程上异步执行还是在主线程上顺序执行。否则,例如,我的单元测试可能会失败,这取决于执行条件。
我在MinGW-w 64邮件列表中发布了一条消息:https://sourceforge.net/p/mingw-w64/mailman/message/34896011/。作为讨论线程的结果,我的解决方案是链接对象文件CRT_fp8.o
。
将编译命令修改为g++ -o Test.exe Test.cpp -O2 c:\mingw-w64-10.2\mingw64\x86_64-w64-mingw32\lib\CRT_fp8.o
(路径可能需要调整)会导致所有线程都以双精度执行浮点运算。现在不同线程的结果将不再不同。
这对我来说是一个很好的解决方案已经有好几年了。然而,几周前在玩不同的编译器版本时,我发现链接到CRT_fp8.o
的解决方案并不像我预期的那样稳定。
当使用g++ 10.2编译,然后更改路径以包含g++ 10.3的“bin”文件夹时,线程将再次产生不同的结果。我可以在这里使用这些控制台命令重现它:
set path=c:\mingw-w64-10.2\mingw64\bin
g++ -o Test.exe Test.cpp -O2 c:\mingw-w64-10.2\mingw64\x86_64-w64-mingw32\lib\CRT_fp8.o
C:\...Path...>Test.exe
Thread id: 1
Thread id: 2
The results are the same.
set path=c:\mingw-w64-10.3\mingw64\bin
C:\...Path...>Test.exe
Thread id: 1
Thread id: 2
The results are different.
这又是一个非常糟糕的情况!如果我的应用程序的用户碰巧在他的路径中有错误的库,他将得到不同的结果!!!:-(
另一个令人惊讶的是,当只使用g++ 10.3时,CRT_fp8.o
的解决方案似乎是不必要的:
set path=c:\mingw-w64-10.3\mingw64\bin
g++ -o Test.exe Test.cpp -O2
C:\...Path...>Test.exe
Thread id: 1
Thread id: 2
The results are the same.
我曾经在MinGW-w 64“bin”文件夹中使用过共享库,发现行为取决于文件“libwinpthread-1.dll”。如果我将文件从g++ 10.2安装复制到g++ 10.3安装,覆盖其自己的共享库,那么行为又像过去几年一样:需要链接到CRT_fp8.o以在所有线程上获得双精度。
这是一个bug?MinGW-w 64 bug?还是libwinpthreads bug?还是一个特性?我的一致精度解决方案是否过时了?新的解决方案是什么?
4条答案
按热度按时间clj7thdc1#
我相信sqrt(x)和pow(x,0.5)不是完全相同的函数。它应该是,但我猜pow(x,0.5)使用对数进行所有计算。因此可能会引入一个小错误。此外,十进制值0.5可能会根据IEEE 754转换为二进制表示,并且可能与0.5的值不完全相同。
在使用这些函数时,不要记住所有这些陷阱,但它们很难跟踪。
dffbzjpn2#
谢谢-也许任务调度程序是这里的罪魁祸首?当任务调度程序将子线程置于睡眠状态时,它可能会将80位FPU寄存器复制到64位双精度?因此它会在计算中丢失一些精度。除此之外-代码
是一种糟糕的编程实践,它会增加所有步骤中的错误传播。
由于循环运行了10000000次,错误传播可能是如此之大,结果值是完全错误的。阅读书“数字食谱在C”和许多浮点计算描述那里也在保持计算错误尽可能低的意义上。80位FPU的设计是为了适应科学计算有10个有效数字是正确的,我想是吧
现在很难记起来了,因为很多年前,我是真实的的数字处理业务。有趣的问题,虽然。但你应该考虑重新设计你的代码,它不需要那么多的迭代。
或者你可以使用128位浮点数,即“长双精度”。长双精度将大大降低错误传播。但我怀疑sqrt(x)和pow(x,0.5)即使使用长双精度也会产生完全相同的结果。
这些功能中的大多数扩展成无限数学级数,并且受到FPU的(有限)设计的限制。
一些图形库实际上可以运行得更快,如果他们依赖于正弦和余弦值的表查找。内存占用现在不再是任何问题。并且您将在每个计算步骤中保存一些CPU/FPU周期。
f87krz0w3#
即使在一个编译器上比较
sqrt(x)
和pow(x,0.5)
的结果,结果也可能不同。这显然比pow
简单。这意味着如果你用GCC 10.2编译,你会得到一个内联的sqrt
,但是当你用10.3运行时DLL运行它时。你链接到那个pow
,所以你甚至没有比较相同的版本。CRT_fp8.o
所做的是提供一个替代的_fpreset
功能,将FPU重置为替代的默认状态-不是80位MinGW默认值,而是64位精度。请注意,MinGW是将GCC硬塞进Windows的一个令人印象深刻的尝试,但GCC在其起源上是一个Stallman项目,具有很强的Unix假设。
最后,这个问题最好通过迁移到x64来完全避免。这应该使用SSE 2来处理所有的FP数学。由于SSE 2从来都不是80位,所以总是得到64位。
cfh9epnr4#
所描述的行为是一个MinGW-w 64错误。与原始帖子中的描述不同,它与g++版本无关,而仅与MinGW-w 64版本有关。在这里报告:https://sourceforge.net/p/mingw-w64/bugs/897/。
这是什么意思?
使用mingw-builds,可以在构建编译器时指定MinGW版本。版本7是安全的,版本8引入了所描述的错误。因此版本7可以用作解决方案。