VC++14.0(2015)编译器中的错误

Bug in VC++ 14.0 (2015) compiler?

本文关键字:错误 编译器 2015 VC++14      更新时间:2023-10-16

我遇到了一些问题,这些问题只发生在Release x86模式期间,而不是在Release x64或任何调试方式期间。我设法使用以下代码重现了这个错误:

#include <stdio.h>
#include <iostream>
using namespace std;
struct WMatrix {
float _11, _12, _13, _14;
float _21, _22, _23, _24;
float _31, _32, _33, _34;
float _41, _42, _43, _44;
WMatrix(float f11, float f12, float f13, float f14,
float f21, float f22, float f23, float f24,
float f31, float f32, float f33, float f34,
float f41, float f42, float f43, float f44) :
_11(f11), _12(f12), _13(f13), _14(f14),
_21(f21), _22(f22), _23(f23), _24(f24),
_31(f31), _32(f32), _33(f33), _34(f34),
_41(f41), _42(f42), _43(f43), _44(f44) {
}
};
void printmtx(WMatrix m1) {
char str[256];
sprintf_s(str, 256, "%.3f, %.3f, %.3f, %.3f", m1._11, m1._12, m1._13, m1._14);
cout << str << "n";
sprintf_s(str, 256, "%.3f, %.3f, %.3f, %.3f", m1._21, m1._22, m1._23, m1._24);
cout << str << "n";
sprintf_s(str, 256, "%.3f, %.3f, %.3f, %.3f", m1._31, m1._32, m1._33, m1._34);
cout << str << "n";
sprintf_s(str, 256, "%.3f, %.3f, %.3f, %.3f", m1._41, m1._42, m1._43, m1._44);
cout << str << "n";
}
WMatrix mul1(WMatrix m, float f) {
WMatrix out = m;
for (unsigned int i = 0; i < 4; i++) {
for (unsigned int j = 0; j < 4; j++) {
unsigned int idx = i * 4 + j; // critical code
*(&out._11 + idx) *= f; // critical code
}
}
return out;
}
WMatrix mul2(WMatrix m, float f) {
WMatrix out = m;
unsigned int idx2 = 0;
for (unsigned int i = 0; i < 4; i++) {
for (unsigned int j = 0; j < 4; j++) {
unsigned int idx = i * 4 + j; // critical code
bool b = idx == idx2; // critical code
*(&out._11 + idx) *= f; // critical code
idx2++;
}
}
return out;
}

int main() {
WMatrix m1(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
WMatrix m2 = mul1(m1, 0.5f);
WMatrix m3 = mul2(m1, 0.5f);
printmtx(m1);
cout << "n";
printmtx(m2);
cout << "n";
printmtx(m3);
int x;
cin >> x;
}

在上面的代码中,mul2起作用,但mul1不起作用。mul1和mul2只是试图迭代WMatrix中的浮点值,并将其乘以f,但mul1索引(i*4+j)的方式不知何故计算出了错误的结果。mul2所做的不同之处在于,它在使用索引之前先检查索引,然后它才能工作(还有许多其他方法可以修改索引以使其工作)。请注意,如果删除行"bool b=idx==idx2",则mul2也会中断。。。

这是输出:

1.000, 2.000, 3.000, 4.000
5.000, 6.000, 7.000, 8.000
9.000, 10.000, 11.000, 12.000
13.000, 14.000, 15.000, 16.000
0.500, 0.500, 0.375, 0.250
0.625, 1.500, 3.500, 8.000
9.000, 10.000, 11.000, 12.000
13.000, 14.000, 15.000, 16.000
0.500, 1.000, 1.500, 2.000
2.500, 3.000, 3.500, 4.000
4.500, 5.000, 5.500, 6.000
6.500, 7.000, 7.500, 8.000

正确的输出应该是…

1.000, 2.000, 3.000, 4.000
5.000, 6.000, 7.000, 8.000
9.000, 10.000, 11.000, 12.000
13.000, 14.000, 15.000, 16.000
0.500, 1.000, 1.500, 2.000
2.500, 3.000, 3.500, 4.000
4.500, 5.000, 5.500, 6.000
6.500, 7.000, 7.500, 8.000
0.500, 1.000, 1.500, 2.000
2.500, 3.000, 3.500, 4.000
4.500, 5.000, 5.500, 6.000
6.500, 7.000, 7.500, 8.000

我是不是错过了什么?或者它实际上是编译器中的一个错误?

这只影响32位编译器;x86-64内部版本不受影响,无论优化设置如何。然而,无论是针对速度(/O2)还是大小(/O1)进行优化,您都可以在32位构建中看到问题的明显表现。正如您所提到的,它在禁用优化的情况下调试构建时可以正常工作。

Wimmel关于改变包装的建议虽然准确,但并没有改变行为。(以下代码假设WMatrix的包装正确设置为1。)

我不能在VS 2010中复制它,但我可以在VS 2013和2015中复制。我没有安装2012。不过,这足以让我们分析两个编译器生成的目标代码之间的差异。

以下是VS 2010中mul1的代码("工作"代码):
(实际上,在许多情况下,编译器在调用站点内联了该函数的代码。但编译器仍然会在内联之前输出包含为各个函数生成的代码的反汇编文件。这就是我们在这里看到的,因为它更混乱。无论是否内联,代码的行为都是完全等效的。)

PUBLIC  mul1
_TEXT   SEGMENT
_m$ = 8                     ; size = 64
_f$ = 72                        ; size = 4
mul1 PROC
___$ReturnUdt$ = eax
push    esi
push    edi
; WMatrix out = m;
mov ecx, 16                 ; 00000010H
lea esi, DWORD PTR _m$[esp+4]
mov edi, eax
rep movsd
; for (unsigned int i = 0; i < 4; i++)
; {
;    for (unsigned int j = 0; j < 4; j++)
;    {
;       unsigned int idx = i * 4 + j; // critical code
;       *(&out._11 + idx) *= f; // critical code
movss   xmm0, DWORD PTR [eax]
cvtps2pd xmm1, xmm0
movss   xmm0, DWORD PTR _f$[esp+4]
cvtps2pd xmm2, xmm0
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss   DWORD PTR [eax], xmm1
movss   xmm1, DWORD PTR [eax+4]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss   DWORD PTR [eax+4], xmm1
movss   xmm1, DWORD PTR [eax+8]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss   DWORD PTR [eax+8], xmm1
movss   xmm1, DWORD PTR [eax+12]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss   DWORD PTR [eax+12], xmm1
movss   xmm2, DWORD PTR [eax+16]
cvtps2pd xmm2, xmm2
cvtps2pd xmm1, xmm0
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss   DWORD PTR [eax+16], xmm1
movss   xmm1, DWORD PTR [eax+20]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss   DWORD PTR [eax+20], xmm1
movss   xmm1, DWORD PTR [eax+24]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss   DWORD PTR [eax+24], xmm1
movss   xmm1, DWORD PTR [eax+28]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss   DWORD PTR [eax+28], xmm1
movss   xmm1, DWORD PTR [eax+32]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss   DWORD PTR [eax+32], xmm1
movss   xmm1, DWORD PTR [eax+36]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss   DWORD PTR [eax+36], xmm1
movss   xmm2, DWORD PTR [eax+40]
cvtps2pd xmm2, xmm2
cvtps2pd xmm1, xmm0
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss   DWORD PTR [eax+40], xmm1
movss   xmm1, DWORD PTR [eax+44]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss   DWORD PTR [eax+44], xmm1
movss   xmm2, DWORD PTR [eax+48]
cvtps2pd xmm1, xmm0
cvtps2pd xmm2, xmm2
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss   DWORD PTR [eax+48], xmm1
movss   xmm1, DWORD PTR [eax+52]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
movss   DWORD PTR [eax+52], xmm1
movss   xmm1, DWORD PTR [eax+56]
cvtps2pd xmm1, xmm1
cvtps2pd xmm2, xmm0
mulsd   xmm1, xmm2
cvtpd2ps xmm1, xmm1
cvtps2pd xmm0, xmm0
movss   DWORD PTR [eax+56], xmm1
movss   xmm1, DWORD PTR [eax+60]
cvtps2pd xmm1, xmm1
mulsd   xmm1, xmm0
pop edi
cvtpd2ps xmm0, xmm1
movss   DWORD PTR [eax+60], xmm0
pop esi
; return out;
ret 0
mul1 ENDP

将其与VS 2015:生成的mul1代码进行比较

mul1 PROC
_m$ = 8                         ; size = 64
; ___$ReturnUdt$ = ecx
; _f$ = xmm2s
; WMatrix out = m;
movups  xmm0, XMMWORD PTR _m$[esp-4]
; for (unsigned int i = 0; i < 4; i++)
xor eax, eax
movaps  xmm1, xmm2
movups  XMMWORD PTR [ecx], xmm0
movups  xmm0, XMMWORD PTR _m$[esp+12]
shufps  xmm1, xmm1, 0
movups  XMMWORD PTR [ecx+16], xmm0
movups  xmm0, XMMWORD PTR _m$[esp+28]
movups  XMMWORD PTR [ecx+32], xmm0
movups  xmm0, XMMWORD PTR _m$[esp+44]
movups  XMMWORD PTR [ecx+48], xmm0
npad    4
$LL4@mul1:
; for (unsigned int j = 0; j < 4; j++)
; {
;    unsigned int idx = i * 4 + j; // critical code
;    *(&out._11 + idx) *= f; // critical code
movups  xmm0, XMMWORD PTR [ecx+eax*4]
mulps   xmm0, xmm1
movups  XMMWORD PTR [ecx+eax*4], xmm0
inc eax
cmp eax, 4
jb  SHORT $LL4@mul1
; return out;
mov eax, ecx
ret 0
?mul1@@YA?AUWMatrix@@U1@M@Z ENDP            ; mul1
_TEXT   ENDS

很明显,代码短了多少。显然,优化器在VS 2010和VS 2015之间变得更聪明了。不幸的是,有时优化器"聪明"的来源是利用代码中的错误。

查看与循环匹配的代码,可以看到VS2010正在展开循环。所有的计算都是内联完成的,所以没有分支。这正是您对具有编译时已知的上界和下界的循环的期望,在本例中,这些上界和下界相当小。

VS 2015发生了什么?嗯,它没有展开任何东西。有5行代码,然后有条件地将JB跳回到循环序列的顶部。仅凭这一点并不能告诉你多少。看起来非常可疑的是,它只循环了4次(请参阅cmp eax, 4语句,该语句在执行jb之前设置了标志,只要计数器小于4,就有效地继续循环)。好吧,如果它把两个循环合并为一个,那可能没问题。让我们看看它在循环的内部做了什么:

$LL4@mul1:
movups  xmm0, XMMWORD PTR [ecx+eax*4]   ; load a packed unaligned value into XMM0
mulps   xmm0, xmm1                      ; do a packed multiplication of XMM0 by XMM1,
;  storing the result in XMM0
movups  XMMWORD PTR [ecx+eax*4], xmm0   ; store the result of the previous multiplication
;  back into the memory location that we
;  initially loaded from
inc      eax                            ; one iteration done, increment loop counter
cmp      eax, 4                         ; see how many loops we've done
jb       $LL4@mul1                      ; keep looping if < 4 iterations

该代码从内存中读取一个值(来自ecx + eax * 4确定的位置的XMM大小的值)到XMM0中,将其乘以XMM1中的值(基于f参数在循环外设置),然后将结果存储回原始内存位置。

将其与mul2:中相应循环的代码进行比较

$LL4@mul2:
lea     eax, DWORD PTR [eax+16]
movups  xmm0, XMMWORD PTR [eax-24]
mulps   xmm0, xmm2
movups  XMMWORD PTR [eax-24], xmm0
sub     ecx, 1
jne     $LL4@mul2

除了不同的循环控制序列(这在循环外将ECX设置为4,每次减去1,并在ECX!=0时保持循环),这里最大的区别是它在内存中处理的实际XMM值。它不是从[ecx+eax*4]加载,而是从[eax-24]加载(在之前已将16添加到EAX之后)。

mul2有什么不同?您添加了代码来跟踪idx2中的一个单独索引,每次循环都会递增。现在,光靠这些是不够的。如果注释掉对bool变量b的赋值,则mul1mul2会产生相同的对象代码。显然,在不比较idxidx2的情况下,编译器能够推断出idx2是完全未使用的,并因此消除它,将mul2变成mul1。但通过这种比较,编译器显然无法消除idx2,它的存在略微改变了函数可能的优化,从而导致输出差异。

现在问题转向为什么会发生这种情况。这是一个优化器错误吗,正如您最初怀疑的那样?好吧,不——正如一些评论者所提到的,永远不要成为你指责编译器/优化器的第一本能。除非你能证明其他情况,否则一定要假设你的代码中有错误。这种证明总是需要查看反汇编,如果你真的想被认真对待,最好参考语言标准的相关部分。

在这种情况下,Mystic已经解决了这个问题。您的代码在执行*(&out._11 + idx)时显示出未定义的行为。这对WMatrix结构在内存中的布局做出了某些假设,即使在明确设置了打包之后,也不能合法地做出这些假设。

这就是为什么未定义的行为是邪恶的——它会导致有时看起来有效,但有时却无效。它对编译器标志非常敏感,尤其是优化,但也对目标平台非常敏感(正如我们在这个答案的顶部所看到的)。mul2只是偶然工作。mul1mul2都是错误的。不幸的是,错误在您的代码中。更糟糕的是,编译器没有发出警告,可能会提醒您使用未定义的行为。

如果我们查看生成的代码,问题就相当清楚了。忽略与当前问题无关的一些位和块,mul1生成如下代码:

movss   xmm1, DWORD PTR _f$[esp-4] ; load xmm1 from _11 of source
; ...
shufps  xmm1, xmm1, 0               ; duplicate _11 across floats of xmm1
; ...
for ecx = 0 to 3 {
movups  xmm0, XMMWORD PTR [dest+ecx*4] ; load 4 floats from dest
mulps   xmm0, xmm1                     ; multiply each by _11
movups  XMMWORD PTR [dest+ecx*4], xmm0 ; store result back to dest
}

因此,不是将一个矩阵的每个元素乘以另一个矩阵对应的元素,而是将一个阵列的每个元素相乘,乘以另一矩阵的_11

尽管不可能确切地确认它是如何发生的(如果不查看编译器的源代码),但这肯定符合@Mysticial对问题如何产生的猜测。