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

时间:2016-08-29 15:11:24

标签: c++ visual-studio

我遇到了一些问题,这些问题只发生在发布x86 模式,而不是发布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

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

2 个答案:

答案 0 :(得分:4)

这只折磨32位编译器;无论优化设置如何,x86-64版本都不会受到影响。但是,无论是针对速度(/ O2)还是针对大小(/ O1)进行优化,您都会看到32位构建中的问题。正如您所提到的,它在调试版本时可以正常工作,并且禁用了优化。

Wimmel建议改变包装,虽然准确,但不会改变行为。 (以下代码假定WMatrix的包装正确设置为1。)

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

以下是来自VS 2010的mul1代码(&#34;工作&#34;代码):
(实际上,在许多情况下,编译器在调用站点内联来自此函数的代码。但编译器仍将输出反汇编文件,其中包含在内联之前为各个函数生成的代码这就是我们在这里看到的内容,因为它更加混乱。无论是否内联代码,代码的行为都是完全相同的。)

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之间获得了更好的批次。不幸的是,有时优化器的来源是智能&#34;是在你的代码中利用bug。

查看与循环匹配的代码,您可以看到VS 2010正在展开循环。所有计算都是内联完成的,因此没有分支。这是您对编译时已知的具有上限和下限的循环的期望,并且在这种情况下,相当小。

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

代码从内存中读取一个值(一个XMM大小的值,从ecx + eax * 4确定的位置)到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,并且它的存在只会稍微改变对函数可能的优化,导致输出差异。

现在问题转向为什么会发生这种情况。它是你最初怀疑的优化错误吗?好吧,不 - 并且正如一些评论者提到的那样,它应该永远不会成为你责备编译器/优化器的第一直觉。始终假设您的代码中存在错误,除非您能证明不是这样。该证据总是涉及反汇编,如果您真的想要认真对待,最好参考语言标准的相关部分。

在这种情况下,Mystical has already nailed the problem*(&out._11 + idx)时,您的代码会显示未定义的行为。这对内存中WMatrix结构的布局做出了某些假设,即使在明确设置打包后,您也无法合法地做出这些假设。

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

答案 1 :(得分:2)

如果我们查看生成的代码,问题就很清楚了。忽略与手头问题无关的一些零碎,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关于问题是如何产生的猜测。