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

时间:2017-06-22 05:54:41

标签: c++ floating-point x86-64 sse libstdc++

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。

1 个答案:

答案 0 :(得分:9)

是的,我重复这个。好吧,主要是。我实际上没有输出0,但其他一些垃圾输出。所以我可以重现无效的行为,我已经确定了原因。

您可以使用-m64 -mno-sse标记here on Goldbolt's Compiler Explorer查看GCC 5.4.0生成的代码。特别是,这些是我们关心的指示:

// 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指令的代码(以及任何后续的指令集扩展)。因此,这意味着必须使用旧版x87 FPU完成所有浮点操作。这样可以正常工作,并且在32位版本上得到了很好的支持,但在64位版本中它是无意义的。 AMD64规范至少需要SSE2支持,因此可以假设所有 64位的x86 CPU都支持SSE和SSE2。这个假设已经进入the ABI x86-64上的所有浮点运算都是使用SSE2指令完成的,浮点值是在XMM寄存器中传递的。因此,执行浮点运算但禁止编译器使用SSE / SSE2指令会使代码生成器处于不可能的位置并导致不可避免的失败。

它到底是怎么失败的?让我们来看看上面的代码。它没有经过优化(因为你没有通过优化标记,它默认为-O0),这使得它有点难以阅读,但请耐心等待。

在第一个块中,它使用x87 FPU指令从存储器加载双精度浮点值(3.14)(它作为二进制中的常量存储)到x87 FPU堆栈顶部的寄存器中。然后,它将该值从堆栈中弹出并将其存储到内存中(程序堆栈)。这完全是在未经优化的代码中完成的繁忙工作,你几乎可以忽略它。这里的结果是你的浮点值存储在rbp-8的内存中(与基指针相差8个字节)。

可以完全忽略下一个指令块。他们只输出字符串&#34; pi:&#34;。

第三个指令块假设输出浮点值。首先,在堆栈上分配8个字节的空间。然后,我们先前存储到内存中的浮点值被压入堆栈。

到目前为止,这么好。这就是通常将一个浮点参数传递给一个函数的方式 - 也就是说,在32位构建中,遵循32位ABI,您使用的是x87指令。在64位构建中,遵循64位ABI,浮点参数应该在XMM寄存器中传递,这是operator<<(double)函数期望接收其参数的位置。 但是,你告诉编译器它不能生成SSE代码,所以它不能使用XMM寄存器。它的双手并列。它无法正确调用ABI之后的库函数,因为您的特定选项打破了 ABI。

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

既然我们了解了这个问题,那么解决方案是什么?

  • 如果您不想使用SSE指令,请强制使用-m32标记编译32位二进制文​​件。这可以安全地与-mno-sse结合使用
  • 如果您需要64位二进制文​​件,则不要传递-mno-sse标志,因为这违反了64位ABI,它假设SSE2支持最小。

(虽然我在这里忽略它,但在技术上合理地传递-mno-sse标志和-m64标志是合理的。事实上,这是明确支持的由GCC用于编译Linux内核代码,其中XMM寄存器状态在调用之间不存在。这仅仅是因为内核代码不执行浮点运算。-mno-sse开关仅用于防止编译器将SSE指令用作与浮点运算无关的高级优化的一部分。)