2017-06-22 45 views
7

TEST.CPP:SSE和iostream的:错误输出,用于浮点类型

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

+2

你的左手不知道右手是doi ng,你也必须重建所有的运行时支持库。这确实很匆忙。 –

回答

9

是的,我重复这一点。好吧,主要是。我其实没有得到0的输出,但有一些其他的垃圾输出。所以我可以重现这种无效的行为,并且我确定了原因。

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

// 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处理器的都支持SSE和SSE2。这个假设已将它变为the ABIx86-64上的所有浮点操作都是使用SSE2指令完成的,并且浮点值在XMM寄存器中传递。因此,做浮点操作但禁止编译器使用SSE/SSE2指令会使代码生成器处于不可能的位置,并导致不可避免的故障。

它究竟如何失败?我们来看看上面的代码。它没有优化(因为你没有通过一个优化标志,它默认为-O0),这使它有点难以阅读,但忍受着我。

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

下一块指令可以完全忽略。他们只输出字符串“pi:”。

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

到目前为止,这么好。这就是你的通常会传递一个浮点参数给一个函数,也就是说,在一个32位版本中,在你使用x87指令的32位ABI之后。在64位版本中,在64位ABI之后,浮点参数应该在XMM寄存器中传递,这是函数期望接收其参数的地方。 但是,你告诉编译器它不能生成SSE代码,所以它不能使用XMM寄存器。它的手绑在一起。它不能正确调用ABI之后的库函数,因为您的特定选项会打破 ABI。

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

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

  • 如果你不想使用SSE指令,强制32位二进制使用-m32标志进行编译。这与-mno-sse安全结合。
  • 如果您需要一个64位二进制文​​件,那么不要传递-mno-sse标志,因为这违反了64位ABI,它将SSE2支持作为最低限度。

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

+0

当我尝试做这样的事情时,我应该会收到警告或错误。编译器意识到我正在连接适合ABI x64的库。对? – olegkhr

+2

查看最后一段。这在技术上是64位构建的有效选项,并且在某些情况下使用,因此它不会是错误。它只是假定你没有做任何浮点运算,因此不会调用标准库中的任何浮点函数。我想编译器理论上可能会检测到你正在做这样一个函数调用并发出一个诊断信息,但它肯定不是必需的,而且我想这对于一个非常罕见的错误来说是一笔很大的开发工作。 @olegkhr –