SSE和iostream:浮点类型的错误输出

SSE and iostream: wrong output for floating point types

本文关键字:类型 错误 输出 iostream SSE      更新时间:2023-10-16

test.cpp:

#include <iostream>
using namespace std;
int main()
{
    double pi = 3.14;
    cout << "pi:"<< pi << endl;
}

g++ -mno-sse test.cpp在Cygwin 64位上编译时,输出为:

pi:0

但是,如果使用g++ test.cpp编译,则可以正常工作。

我有GCC版本5.4.0。

是的,我再生它。好,主要是。实际上,我没有获得0的输出,而是其他一些垃圾输出。这样我就可以重现无效的行为,并且已经指出了原因。

您可以看到GCC 5.4.0在Goldbolt的编译器资源管理器上使用-m64 -mno-sse标志生成的代码。特别是,这些是我们关心的说明:

// double pi = 3.14;
fld     QWORD PTR .LC0[rip]
fstp    QWORD PTR [rbp-8]
// std::cout << "pi:";
mov     esi, OFFSET FLAT:.LC1
mov     edi, OFFSET FLAT:std::cout
call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
// std::cout << pi;
sub     rsp, 8
push    QWORD PTR [rbp-8]
mov     rdi, rax
call    std::basic_ostream<char, std::char_traits<char> >::operator<<(double)
add     rsp, 16

这里发生了什么?好吧,首先,我们需要了解-mno-sse标志的含义。这样可以防止编译器生成使用SSE指令(以及任何以后的说明集扩展程序(的任何代码。因此,这意味着必须使用Legacy X87 FPU进行所有浮点操作。这效果很好,并且在32位版本上得到了很好的支持,但是在64位版本上是荒谬的。AMD64规范需要SSE2支持作为最小值,因此可以假定所有所有 64位能力的X86 CPU都将支持SSE和SSE2。此假设已进入ABI: X86-64上的所有浮点操作都是使用SSE2指令完成的,并且在XMM寄存器中传递了浮点值。因此,执行浮点操作,但禁止编译器使用SSE/SSE2指令将代码生成器处于不可能的位置,并导致不可避免的故障。

它到底是如何失败的?让我们浏览上面的代码。它是不优化的(由于您没有传递优化标志,因此默认为-O0(,这使得很难阅读,但请与我同意。

在第一个块中,它使用x87 FPU指令从内存(将其作为二进制中的常数存储在X87 FPU堆栈顶部的寄存器中(加载双精度浮点数(3.14((3.14(。然后,它弹出该值,并将其存储在内存中( program 堆栈(。这完全只是在不优化的代码中完成的忙碌工作,您几乎可以忽略它。这里的结果是您的浮点值存储在rbp-8的内存中(从基本指针中偏移了8个字节(。

下一个指令可以完全忽略。他们只是输出字符串" pi:"。

指令的第三个块是假定的以输出浮点值。首先,在堆栈上分配了8个字节。然后,我们以前存储在存储器上的浮点值被推到堆栈上。

到目前为止,一切都很好。这就是您通常通常会将浮点数参数传递给函数的方式,也就是说,在32位构建中,遵循32位ABI,您使用x87指令。在64位构建中,遵循64位ABI,应该在XMM寄存器中传递浮点参数,这是operator<<(double)函数期望接收其参数的地方。但是,您告诉编译器它无法生成SSE代码,因此无法使用XMM寄存器。它的手被绑住。它无法正确调用遵循ABI的库函数,因为您的特定选项 break abi。

这一切都从这里下坡。编译器将rax寄存器的内容复制到rdi寄存器中,然后调用operator<<(double)函数。此函数试图在XMM0寄存器中写入传递的浮点值,但该寄存器包含垃圾(在您的情况下,它似乎包含0,但其实际内容正式未定义(,因此该垃圾写入STDOUT,而不是您期望看到的浮点值。

现在我们了解问题了,解决方案是什么?

  • 如果您不想使用SSE指令,请使用-m32标志强制32位二进制文件。这将与-mno-sse安全联合使用。
  • 如果您需要64位二进制文件,则不要传递-mno-sse标志,因为这是对64位ABI的违规,它假设SSE2支持最低。

(尽管我在这里忽略了它,但在技术上是-mno-sse标志一起通过-m64标志合理的。的确,GCC明确支持了这一点,因为它用于编译Linux内核代码,XMM寄存器的状态在呼叫之间不持续。此作用仅是因为内核代码不执行浮点操作。-mno-sse交换机仅用于防止编译器使用SSE指令作为具有已有高级优化的一部分与浮点操作无关。(