c++ 为什么MinGW-w 64浮点精度取决于winpthreads版本?

6mzjoqzu  于 2023-04-08  发布在  其他
关注(0)|答案(4)|浏览(158)

我使用的是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?还是一个特性?我的一致精度解决方案是否过时了?新的解决方案是什么?

clj7thdc

clj7thdc1#

我相信sqrt(x)和pow(x,0.5)不是完全相同的函数。它应该是,但我猜pow(x,0.5)使用对数进行所有计算。因此可能会引入一个小错误。此外,十进制值0.5可能会根据IEEE 754转换为二进制表示,并且可能与0.5的值不完全相同。
在使用这些函数时,不要记住所有这些陷阱,但它们很难跟踪。

dffbzjpn

dffbzjpn2#

谢谢-也许任务调度程序是这里的罪魁祸首?当任务调度程序将子线程置于睡眠状态时,它可能会将80位FPU寄存器复制到64位双精度?因此它会在计算中丢失一些精度。除此之外-代码

for (i = 0, a = 1.0; i < 10000000; i++)
    {
        a *= 1.00000001;
        b = sqrt(a);
        c = pow(a, 0.5);
    }

是一种糟糕的编程实践,它会增加所有步骤中的错误传播。
由于循环运行了10000000次,错误传播可能是如此之大,结果值是完全错误的。阅读书“数字食谱在C”和许多浮点计算描述那里也在保持计算错误尽可能低的意义上。80位FPU的设计是为了适应科学计算有10个有效数字是正确的,我想是吧
现在很难记起来了,因为很多年前,我是真实的的数字处理业务。有趣的问题,虽然。但你应该考虑重新设计你的代码,它不需要那么多的迭代。
或者你可以使用128位浮点数,即“长双精度”。长双精度将大大降低错误传播。但我怀疑sqrt(x)和pow(x,0.5)即使使用长双精度也会产生完全相同的结果。
这些功能中的大多数扩展成无限数学级数,并且受到FPU的(有限)设计的限制。
一些图形库实际上可以运行得更快,如果他们依赖于正弦和余弦值的表查找。内存占用现在不再是任何问题。并且您将在每个计算步骤中保存一些CPU/FPU周期。

f87krz0w

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位。

cfh9epnr

cfh9epnr4#

所描述的行为是一个MinGW-w 64错误。与原始帖子中的描述不同,它与g++版本无关,而仅与MinGW-w 64版本有关。在这里报告:https://sourceforge.net/p/mingw-w64/bugs/897/
这是什么意思?
使用mingw-builds,可以在构建编译器时指定MinGW版本。版本7是安全的,版本8引入了所描述的错误。因此版本7可以用作解决方案。

相关问题