SSE 和 iostream:浮点类型的错误输出
SSE and iostream: wrong output for floating point types
test.cpp:
#include <iostream>
using namespace std;
int main()
{
double pi = 3.14;
cout << "pi:"<< pi << endl;
}
在 cygwin 64 位上使用 g++ -mno-sse test.cpp
编译时,输出为:
pi:0
但是,如果使用 g++ test.cpp
编译,它可以正常工作。
我有 GCC 5.4.0 版。
是的,我复制这个。好吧,主要是。我实际上没有得到 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 CPU 将同时支持 SSE 和 SSE2。这个假设变成了 the ABI:x86-64 上的所有浮点运算都是使用 SSE2 指令完成的,并且浮点值在 XMM 寄存器中传递。因此,进行浮点运算但禁止编译器使用 SSE/SSE2 指令会使代码生成器处于不可能的位置并导致不可避免的失败。
具体是怎么失败的?让我们来看看上面的代码。它是未优化的(因为你没有传递优化标志,它默认为 -O0
),这使得它有点难以阅读,但请耐心等待。
在第一个块中,它使用 x87 FPU 指令将您的双精度浮点值 (3.14) 从内存(它作为二进制常量存储)加载到 x87 顶部的寄存器中FPU 堆栈。然后,它将该值从堆栈中弹出并将其存储到内存中(program 堆栈)。这完全只是在未优化代码中完成的繁忙工作,您几乎可以忽略它。这里的结果是您的浮点值存储在内存中的 rbp-8
(距基指针 8 个字节的偏移量)。
下一段指令可以完全忽略。他们只是输出字符串 "pi:".
第三块指令应该输出浮点值。首先,在堆栈上分配 8 个字节的 space。然后,我们之前存储在内存中的浮点值被压入堆栈。
到目前为止,还不错。这就是您 通常 将浮点参数传递给函数的方式——也就是说,在 32 位构建中,遵循 32 位 ABI,您在其中使用 x87 指令。在 64 位构建中,遵循 64 位 ABI,浮点参数应该在 XMM 寄存器中传递,这是 operator<<(double)
函数期望接收其参数的地方。 但是,你告诉编译器它不能生成SSE代码,所以它不能使用XMM寄存器。它的手被绑住了。它无法正确调用 ABI 之后的库函数,因为您的特定选项 break ABI。
从这里开始都是下坡路。编译器将rax
寄存器的内容复制到rdi
寄存器中,然后调用operator<<(double)
函数。此函数尝试将 XMM0
寄存器中传递的浮点值写入 stdout,但该寄存器包含垃圾(在您的情况下,它似乎包含 0,但其实际内容在形式上未定义),因此此垃圾被写入标准输出,而不是您期望看到的浮点值。
既然我们了解了问题,那么解决方案是什么?
- 如果您不想使用 SSE 指令,请使用
-m32
标志强制编译 32 位二进制文件。 这与 -mno-sse
.
- 如果您需要 64 位二进制文件,则不要传递
-mno-sse
标志,因为这违反了 64 位 ABI,它假定 SSE2 支持作为最小值。
(虽然我在这里忽略它,但技术上 合理地传递 -mno-sse
标志和 -m64
标志。确实,这GCC 明确支持它,因为它用于编译 Linux 内核代码,其中 XMM 寄存器的状态不会在调用之间保留。这仅是因为内核代码不执行浮点运算。-mno-sse
开关仅用于防止编译器将 SSE 指令用作与浮点运算无关的高级优化的一部分。)
test.cpp:
#include <iostream>
using namespace std;
int main()
{
double pi = 3.14;
cout << "pi:"<< pi << endl;
}
在 cygwin 64 位上使用 g++ -mno-sse test.cpp
编译时,输出为:
pi:0
但是,如果使用 g++ test.cpp
编译,它可以正常工作。
我有 GCC 5.4.0 版。
是的,我复制这个。好吧,主要是。我实际上没有得到 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 CPU 将同时支持 SSE 和 SSE2。这个假设变成了 the ABI:x86-64 上的所有浮点运算都是使用 SSE2 指令完成的,并且浮点值在 XMM 寄存器中传递。因此,进行浮点运算但禁止编译器使用 SSE/SSE2 指令会使代码生成器处于不可能的位置并导致不可避免的失败。
具体是怎么失败的?让我们来看看上面的代码。它是未优化的(因为你没有传递优化标志,它默认为 -O0
),这使得它有点难以阅读,但请耐心等待。
在第一个块中,它使用 x87 FPU 指令将您的双精度浮点值 (3.14) 从内存(它作为二进制常量存储)加载到 x87 顶部的寄存器中FPU 堆栈。然后,它将该值从堆栈中弹出并将其存储到内存中(program 堆栈)。这完全只是在未优化代码中完成的繁忙工作,您几乎可以忽略它。这里的结果是您的浮点值存储在内存中的 rbp-8
(距基指针 8 个字节的偏移量)。
下一段指令可以完全忽略。他们只是输出字符串 "pi:".
第三块指令应该输出浮点值。首先,在堆栈上分配 8 个字节的 space。然后,我们之前存储在内存中的浮点值被压入堆栈。
到目前为止,还不错。这就是您 通常 将浮点参数传递给函数的方式——也就是说,在 32 位构建中,遵循 32 位 ABI,您在其中使用 x87 指令。在 64 位构建中,遵循 64 位 ABI,浮点参数应该在 XMM 寄存器中传递,这是 operator<<(double)
函数期望接收其参数的地方。 但是,你告诉编译器它不能生成SSE代码,所以它不能使用XMM寄存器。它的手被绑住了。它无法正确调用 ABI 之后的库函数,因为您的特定选项 break ABI。
从这里开始都是下坡路。编译器将rax
寄存器的内容复制到rdi
寄存器中,然后调用operator<<(double)
函数。此函数尝试将 XMM0
寄存器中传递的浮点值写入 stdout,但该寄存器包含垃圾(在您的情况下,它似乎包含 0,但其实际内容在形式上未定义),因此此垃圾被写入标准输出,而不是您期望看到的浮点值。
既然我们了解了问题,那么解决方案是什么?
- 如果您不想使用 SSE 指令,请使用
-m32
标志强制编译 32 位二进制文件。 这与-mno-sse
. - 如果您需要 64 位二进制文件,则不要传递
-mno-sse
标志,因为这违反了 64 位 ABI,它假定 SSE2 支持作为最小值。
(虽然我在这里忽略它,但技术上 合理地传递 -mno-sse
标志和 -m64
标志。确实,这GCC 明确支持它,因为它用于编译 Linux 内核代码,其中 XMM 寄存器的状态不会在调用之间保留。这仅是因为内核代码不执行浮点运算。-mno-sse
开关仅用于防止编译器将 SSE 指令用作与浮点运算无关的高级优化的一部分。)