错误的乘法结果:未定义的行为或编译器错误?
Wrong result of multiplication: Undefined behavior or compiler bug?
背景
在调试数字库中的问题时,我能够查明数字开始变得不正确的第一个位置。但是,C++ 代码本身似乎是正确的。所以我查看了 Visual Studio 的 C++ 编译器生成的程序集并开始怀疑编译器错误。
代码
我能够在代码的高度简化的隔离版本中重现该行为:
sourceB.cpp:
double alwaysOneB(double a[3]) {
return 1.0;
}
main.cpp:
#include <iostream>
__declspec(noinline)
bool alwaysTrue() {
return true;
}
__declspec(noinline)
double alwaysOneA(const double a[3]) {
return 1.0;
}
double alwaysOneB(double a[3]); // implemented in sourceB.cpp
int main() {
double* result = new double[2];
if (alwaysTrue()) {
double v[3];
v[0] = 0.0;
v[1] = 0.0;
v[2] = 0.0;
alwaysOneB(v);
double d = alwaysOneA(v); // d = 1
std::cout << "d = " << d << std::endl; // output: "d = 1" (as expected)
result[0] = d * v[2];
result[1] = d * d; // should be: 1 * 1 => 1
}
if (alwaysTrue()) {
std::cout << "result[1] = " << result[1] << std::endl; // output: "result[1] = 2.23943e-47" (expected: 1)
}
delete[] result;
return 0;
}
该代码包含一些对其他函数的虚假调用,这些函数(很遗憾)是重现问题所必需的。但是,预期的行为应该仍然非常清楚。 1.0
的值被分配给变量 d
,然后自乘。该结果应再次为 1.0
,它被写入数组并打印到控制台。所以期望的输出是:
d = 1
result[1] = 1
但是得到的输出是:
d = 1
result[1] = 3.77013e+214
测试环境
代码已使用 Visual Studio Community 2019 附带的 C++ 编译器进行测试(最新更新,VS 16.11.9,VC++ 00435-60000-00000-AA327)。该问题仅在 激活优化 (/O2
) 时出现。使用 /Od
编译会生成打印正确输出的二进制文件。
在简化的示例中(不是针对编译完整库时的原始问题)我还必须 停用“完整程序优化”,否则编译器会去掉我伪造的函数调用。
此简化示例仅在为 x86
编译时重现问题(其他示例为 x64
[=87 重现问题=]).
完整编译命令行如下:
/permissive- /ifcOutput "Release\" /GS /analyze- /W3 /Gy /Zc:wchar_t /Zi /Gm- /O2 /sdl /Fd"Release\vc142.pdb" /Zc:inline /fp:precise /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /Gd /Oy- /Oi /MD /FC /Fa"Release\" /EHsc /nologo /Fo"Release\" /Fp"Release\DecimateBug2.pch" /diagnostics:column
要下载的完整 Visual Studio 解决方案:https://drive.google.com/file/d/1EyoX0uXEkvfJ_Fh649k9XjJQPdDUMik7/view?usp=sharing
GNU 编译器和 Clang 都会生成打印所需结果的二进制文件。
问题
这段代码中是否有任何我看不到的未定义行为证明了不正确的结果?或者我应该将其报告为编译器错误?
编译器生成的程序集
对于两条乘法线
result[0] = d * v[2];
result[1] = d * d;
编译器生成以下汇编代码:
00CF1432 movsd xmm1,mmword ptr [esp+18h] // Load d into first part of xmm1
00CF1438 unpcklpd xmm1,xmm1 // Load d into second part of xmm1
00CF143C movups xmm0,xmmword ptr [esp+30h] // Load second operands into xmm0
00CF1441 mulpd xmm0,xmm1 // 2 multiplications at one
00CF1445 movups xmmword ptr [esi],xmm0 // store result
显然它试图使用 mulpd
同时执行两个乘法。在前两行中,它成功地将 d
操作数加载到 xmm1
寄存器的两个部分(作为第一个操作数)。但是当它尝试加载第二个操作数(v[2]
和 d
)时,它只是从 v[2]
地址(esp+30h
)加载 128 位。这对于 first 乘法 (v[2]
) 的第二个操作数没有问题,但对于 second 乘法(d
).显然,代码假设 d
在内存中紧跟在 v
之后。然而,事实并非如此。变量 d
实际上从未存储在内存中,它似乎只存在于寄存器中。
这让我强烈怀疑编译器错误。但是,我想确认我没有遗漏任何证明错误程序集正确的未定义行为。
尽管没有人发布答案,但从评论部分我可以得出结论:
- 没有人在错误重现代码中发现任何未定义的行为。
- 至少你们中的一些人能够重现不良行为。
所以我提交了a bug report against Visual Studio 2019。
微软团队确认了这个问题。
但是,不幸的是 Visual Studio 2019 似乎 不会 收到错误修复,因为 Visual Studio 2022貌似没有bug。显然,没有那个特定错误的最新版本足以满足微软的质量标准。
我觉得这很令人失望,因为我认为编译器的正确性至关重要,而且 Visual Studio 2022 刚刚发布了新功能,因此可能包含新错误。所以没有真正的“稳定版本”(一个是最先进的,另一个没有修复错误)。但我想我们必须忍受这一点,或者选择一个不同的、更稳定的编译器。
背景
在调试数字库中的问题时,我能够查明数字开始变得不正确的第一个位置。但是,C++ 代码本身似乎是正确的。所以我查看了 Visual Studio 的 C++ 编译器生成的程序集并开始怀疑编译器错误。
代码
我能够在代码的高度简化的隔离版本中重现该行为:
sourceB.cpp:
double alwaysOneB(double a[3]) {
return 1.0;
}
main.cpp:
#include <iostream>
__declspec(noinline)
bool alwaysTrue() {
return true;
}
__declspec(noinline)
double alwaysOneA(const double a[3]) {
return 1.0;
}
double alwaysOneB(double a[3]); // implemented in sourceB.cpp
int main() {
double* result = new double[2];
if (alwaysTrue()) {
double v[3];
v[0] = 0.0;
v[1] = 0.0;
v[2] = 0.0;
alwaysOneB(v);
double d = alwaysOneA(v); // d = 1
std::cout << "d = " << d << std::endl; // output: "d = 1" (as expected)
result[0] = d * v[2];
result[1] = d * d; // should be: 1 * 1 => 1
}
if (alwaysTrue()) {
std::cout << "result[1] = " << result[1] << std::endl; // output: "result[1] = 2.23943e-47" (expected: 1)
}
delete[] result;
return 0;
}
该代码包含一些对其他函数的虚假调用,这些函数(很遗憾)是重现问题所必需的。但是,预期的行为应该仍然非常清楚。 1.0
的值被分配给变量 d
,然后自乘。该结果应再次为 1.0
,它被写入数组并打印到控制台。所以期望的输出是:
d = 1
result[1] = 1
但是得到的输出是:
d = 1
result[1] = 3.77013e+214
测试环境
代码已使用 Visual Studio Community 2019 附带的 C++ 编译器进行测试(最新更新,VS 16.11.9,VC++ 00435-60000-00000-AA327)。该问题仅在 激活优化 (/O2
) 时出现。使用 /Od
编译会生成打印正确输出的二进制文件。
在简化的示例中(不是针对编译完整库时的原始问题)我还必须 停用“完整程序优化”,否则编译器会去掉我伪造的函数调用。
此简化示例仅在为 x86
编译时重现问题(其他示例为 x64
[=87 重现问题=]).
完整编译命令行如下:
/permissive- /ifcOutput "Release\" /GS /analyze- /W3 /Gy /Zc:wchar_t /Zi /Gm- /O2 /sdl /Fd"Release\vc142.pdb" /Zc:inline /fp:precise /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /Gd /Oy- /Oi /MD /FC /Fa"Release\" /EHsc /nologo /Fo"Release\" /Fp"Release\DecimateBug2.pch" /diagnostics:column
要下载的完整 Visual Studio 解决方案:https://drive.google.com/file/d/1EyoX0uXEkvfJ_Fh649k9XjJQPdDUMik7/view?usp=sharing
GNU 编译器和 Clang 都会生成打印所需结果的二进制文件。
问题
这段代码中是否有任何我看不到的未定义行为证明了不正确的结果?或者我应该将其报告为编译器错误?
编译器生成的程序集
对于两条乘法线
result[0] = d * v[2];
result[1] = d * d;
编译器生成以下汇编代码:
00CF1432 movsd xmm1,mmword ptr [esp+18h] // Load d into first part of xmm1
00CF1438 unpcklpd xmm1,xmm1 // Load d into second part of xmm1
00CF143C movups xmm0,xmmword ptr [esp+30h] // Load second operands into xmm0
00CF1441 mulpd xmm0,xmm1 // 2 multiplications at one
00CF1445 movups xmmword ptr [esi],xmm0 // store result
显然它试图使用 mulpd
同时执行两个乘法。在前两行中,它成功地将 d
操作数加载到 xmm1
寄存器的两个部分(作为第一个操作数)。但是当它尝试加载第二个操作数(v[2]
和 d
)时,它只是从 v[2]
地址(esp+30h
)加载 128 位。这对于 first 乘法 (v[2]
) 的第二个操作数没有问题,但对于 second 乘法(d
).显然,代码假设 d
在内存中紧跟在 v
之后。然而,事实并非如此。变量 d
实际上从未存储在内存中,它似乎只存在于寄存器中。
这让我强烈怀疑编译器错误。但是,我想确认我没有遗漏任何证明错误程序集正确的未定义行为。
尽管没有人发布答案,但从评论部分我可以得出结论:
- 没有人在错误重现代码中发现任何未定义的行为。
- 至少你们中的一些人能够重现不良行为。
所以我提交了a bug report against Visual Studio 2019。
微软团队确认了这个问题。
但是,不幸的是 Visual Studio 2019 似乎 不会 收到错误修复,因为 Visual Studio 2022貌似没有bug。显然,没有那个特定错误的最新版本足以满足微软的质量标准。
我觉得这很令人失望,因为我认为编译器的正确性至关重要,而且 Visual Studio 2022 刚刚发布了新功能,因此可能包含新错误。所以没有真正的“稳定版本”(一个是最先进的,另一个没有修复错误)。但我想我们必须忍受这一点,或者选择一个不同的、更稳定的编译器。