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

ole*_*khr 7 c++ floating-point sse x86-64 libstdc++

TEST.CPP:

#include <iostream>
using namespace std;

int main()
{
    double pi = 3.14;
    cout << "pi:"<< pi << endl;
}
Run Code Online (Sandbox Code Playgroud)

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

PI:0

但是,如果编译时它可以正常工作g++ test.cpp.

我有GCC版本5.4.0.

Cod*_*ray 9

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

您可以在Goldbolt的Compiler Explorer中看到GCC 5.4.0使用-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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

第三块指令应该输出浮点值.首先,在堆栈上分配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指令作为与浮点运算无关的高级优化的一部分.)

  • 见最后一段.这在技术上是64位构建的有效选项,并且在某些情况下使用,因此它不会是错误.它只是假设您没有进行任何浮点运算,因此不会从标准库中调用任何浮点函数.我认为编译器理论上*可以*检测到您正在进行这样的函数调用并发出诊断信息,但它当然不是必需的,而且我认为这将是一个非常罕见的错误的大量开发人员的努力.@olegkhr (2认同)