Visual Studio 2012 不同的值发布/调试模式

Posted

技术标签:

【中文标题】Visual Studio 2012 不同的值发布/调试模式【英文标题】:Visual Studio 2012 different values Release/Debug mode 【发布时间】:2014-06-10 14:10:30 【问题描述】:

此代码在 MSVS 2012、Windows 7 中在调试和发布模式之间切换时会产生不同的值:

#include <iostream>
using namespace std;

int A[20000];

int main() 

    int shift = 0;
    int Period = 30;
    //Fill array
    for(int i = 0; i < 20000; i++) 
        A[i] = i * 2 + 123;
    

    int sumTotal = 0;
    int sum = 0;

    for(int bars = Period + 10; bars < 1000; bars++) 
        sum = 0;
        for(int i = 0; i< Period; i++) 
            sum += A[bars - i];
        
        sumTotal += sum;
    
    cout << sumTotal << endl;

你能重现或找出原因吗?我一直在测试项目属性的各种设置。

调试(正确结果):32630400 发布:32814720

/GS /GL /analyze- /W3 /Gy /Zc:wchar_t /I"C:\Program Files (x86)\Visual Leak Detector\include" /Z7 /Gm- /O2 /Fd"Release\vc110.pdb" /fp:precise /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /Gd /Oy- /Oi /MD /Fa"Release\" /EHsc /nologo /Fo"Release\" /Fp"Release\Testing.pch"

【问题讨论】:

你得到了哪些不同的值?其中任何一个都是正确的答案吗?另外,请告诉我们您正在使用哪些编译器选项。 SO 规则要求您将问题缩小到最小。例如,+123 真的有必要吗? 我可以重现这个。奇怪的是,如果将 A 更改为 std::array&lt;int, 20000&gt;,差异就消失了。 如果我禁用优化,发布版本会给出与调试版本相同的(根据您的说法是正确的)答案。此外,优化发布版本的反汇编是......奇怪 - 我真的无法理解它试图做什么(尽管我不是那里的专家)。我将暂时得出结论,您发现了一个编译器错误。如果是这样,恭喜。 我能用最简单的程序来演示这个错误:gist.github.com/bcrist/53035b973fb0e6f8ed52 将循环条件更改为i &lt;= 10 或更低会使其消失,(在这种情况下,循环可能会展开) 【参考方案1】:

我使用 VS2012 C 编译器测试了代码的“精简”版本

int main()

  int A[12] =  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ;

  int sum = 0;
  int i;

  for (i = 0; i < 12; ++i)
     sum += A[11 - i];

  printf("%d\n", sum);

  return 0;

我在 x64 模式下编译它 发布配置为速度优化。该错误仍然存​​在,但取决于其他优化和代码生成设置,它会以不同的方式显示自己。一个版本的代码生成“随机”结果,而另一个版本始终生成 8 作为总和(而不是正确的 12)。

对于始终生成 8 的版本,生成的代码如下所示

000000013FC81DF0  mov         rax,rsp  
000000013FC81DF3  sub         rsp,68h  
000000013FC81DF7  movd        xmm1,dword ptr [rax-18h]  
000000013FC81DFC  movd        xmm2,dword ptr [rax-10h]  
000000013FC81E01  movd        xmm5,dword ptr [rax-0Ch]  
000000013FC81E06  xorps       xmm0,xmm0  
000000013FC81E09  xorps       xmm3,xmm3  

for (i = 0; i < 12; ++i)
000000013FC81E0C  xor         ecx,ecx  
000000013FC81E0E  mov         dword ptr [rax-48h],1  
000000013FC81E15  mov         dword ptr [rax-44h],1  
000000013FC81E1C  mov         dword ptr [rax-40h],1  
000000013FC81E23  punpckldq   xmm2,xmm1  
000000013FC81E27  mov         dword ptr [rax-3Ch],1  
000000013FC81E2E  mov         dword ptr [rax-38h],1  
000000013FC81E35  mov         dword ptr [rax-34h],1  

     sum += A[11 - i];
000000013FC81E3C  movdqa      xmm4,xmmword ptr [__xmm@00000001000000010000000100000001 (013FC83360h)]  
000000013FC81E44  paddd       xmm4,xmm0  
000000013FC81E48  movd        xmm0,dword ptr [rax-14h]  
000000013FC81E4D  mov         dword ptr [rax-30h],1  
000000013FC81E54  mov         dword ptr [rax-2Ch],1  
000000013FC81E5B  mov         dword ptr [rax-28h],1  
000000013FC81E62  mov         dword ptr [rax-24h],1  
000000013FC81E69  punpckldq   xmm5,xmm0  
000000013FC81E6D  punpckldq   xmm5,xmm2  
000000013FC81E71  paddd       xmm5,xmm3  
000000013FC81E75  paddd       xmm5,xmm4  
000000013FC81E79  mov         dword ptr [rax-20h],1  
000000013FC81E80  mov         dword ptr [rax-1Ch],1  
000000013FC81E87  mov         r8d,ecx  
000000013FC81E8A  movdqa      xmm0,xmm5  
000000013FC81E8E  psrldq      xmm0,8  
000000013FC81E93  paddd       xmm5,xmm0  
000000013FC81E97  movdqa      xmm0,xmm5  
000000013FC81E9B  lea         rax,[rax-40h]  
000000013FC81E9F  mov         r9d,2  
000000013FC81EA5  psrldq      xmm0,4  
000000013FC81EAA  paddd       xmm5,xmm0  
000000013FC81EAE  movd        edx,xmm5  
000000013FC81EB2  nop         word ptr [rax+rax]  

     sum += A[11 - i];
000000013FC81EC0  add         ecx,dword ptr [rax+4]  
000000013FC81EC3  add         r8d,dword ptr [rax]  
000000013FC81EC6  lea         rax,[rax-8]  
000000013FC81ECA  dec         r9  
000000013FC81ECD  jne         main+0D0h (013FC81EC0h)  


printf("%d\n", sum);
000000013FC81ECF  lea         eax,[r8+rcx]  
000000013FC81ED3  lea         rcx,[__security_cookie_complement+8h (013FC84040h)]  
000000013FC81EDA  add         edx,eax  
000000013FC81EDC  call        qword ptr [__imp_printf (013FC83140h)]  

return 0;
000000013FC81EE2  xor         eax,eax  

000000013FC81EE4  add         rsp,68h  
000000013FC81EE8  ret  

代码生成器和优化器留下了很多奇怪且看似不必要的mumbo-jumbo,但这段代码的作用可以简要描述如下。

这里有两种独立的算法来产生最终的和,它们显然应该处理数组的不同部分。我猜想两个处理流程(非 SSE 和 SSE)用于通过指令流水线促进并行性。

一种算法是一个简单的循环,它对数组元素求和,每次迭代处理两个元素。可以从上面的“交错”代码中提取如下

; Initialization
000000013F1E1E0C  xor         ecx,ecx                 ; ecx - odd element sum
000000013F1E1E87  mov         r8d,ecx                 ; r8 - even element sum
000000013F1E1E9B  lea         rax,[rax-40h]           ; start from i = 2
000000013F1E1E9F  mov         r9d,2                   ; do 2 iterations

; The cycle
000000013F1E1EC0  add         ecx,dword ptr [rax+4]   ; ecx += A[i + 1]
000000013F1E1EC3  add         r8d,dword ptr [rax]     ; r8d += A[i]
000000013F1E1EC6  lea         rax,[rax-8]             ; i -= 2
000000013F1E1ECA  dec         r9                      
000000013F1E1ECD  jne         main+0D0h (013F1E1EC0h) ; loop again if r9 is not zero 

该算法从地址rax - 40h 开始添加元素,在我的实验中等于&amp;A[2],并向后跳过两个元素进行两次迭代。这会在寄存器 r8 中累加 A[0]A[2] 的总和,在寄存器 ecx 中累加 A[1]A[3] 的总和。因此,这部分算法处理数组的 4 个元素,并在 r8ecx 中正确生成值 2

算法的另一部分是使用 SSE 指令编写的,显然负责对数组的剩余部分求和。可以从代码中提取如下

; Initially xmm5 is zero
000000013F1E1E3C  movdqa      xmm4,xmmword ptr [__xmm@00000001000000010000000100000001 (013F1E3360h)]  
000000013F1E1E75  paddd       xmm5,xmm4  

000000013F1E1E8A  movdqa      xmm0,xmm5               ; copy
000000013F1E1E8E  psrldq      xmm0,8                  ; shift
000000013F1E1E93  paddd       xmm5,xmm0               ; and add

000000013F1E1E8A  movdqa      xmm0,xmm5               ; copy
000000013F1E1E8E  psrldq      xmm0,4                  ; shift
000000013F1E1E93  paddd       xmm5,xmm0               ; and add

000000013F1E1EAE  movd        edx,xmm5                ; edx - the sum

该部分使用的通用算法很简单:它将值0x00000001000000010000000100000001 放入128 位寄存器xmm5,然后将其向右移动8 个字节(0x00000000000000000000000100000001)并将其与原始值相加,产生0x00000001000000010000000200000002。这再次向右移动 4 个字节 (0x00000000000000010000000100000002) 并再次添加到前一个值,产生0x00000001000000020000000300000004。将xmm5 的最后一个32 位字0x00000004 作为结果并放入寄存器edx。因此,该算法产生4 作为其最终结果。很明显,该算法只是在 128 位寄存器中执行连续 32 位字的“并行”加法。请注意,顺便说一句,该算法甚至没有尝试访问A,它从编译器/优化器生成的嵌入式常量开始求和。

现在,最后将r8 + ecx + edx 的值报告为最终总和。显然,这只是8,而不是正确的12。看起来这两种算法之一忘记了做一些工作。我不知道是哪一个,但从大量的“冗余”指令来看,它看起来像是应该在edx 中生成8 而不是4 的SSE 算法。一个可疑的指令是这个

000000013FC81E71  paddd       xmm5,xmm3  

在那一刻xmm3 总是包含零。所以,这条指令看起来完全是多余的和不必要的。但是,如果 xmm3 实际上包含另一个“神奇”常量,表示数组的另外 4 个元素(就像 xmm4 所做的那样),那么该算法将正常工作并产生正确的总和。

如果对数组元素使用独特的初始值

int A[12] =  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ;

可以清楚地看到,第一个(非SSE)算法成功求和1, 2, 3, 4,而第二个(SSE)算法求和9, 10, 11, 125, 6, 7, 8 仍然被排除在考虑之外,导致 52 作为最终总和,而不是正确的 78

这绝对是编译器/优化器的错误。

附注导入到 VS2013 Update 2 中的相同设置的相同项目似乎没有受到此错误的影响。

【讨论】:

这是给我的吗? 我的意思是,I tested your "reduced" version of the code @barak manos:我从 MS 错误报告页面的 cmets 中借用了代码。你是作者吗?我认为 OP 也是该代码的作者。 在上面(或下面)查看我的答案。我猜有人在错误报告中添加了它,因为我在这里的任何其他答案中都没有看到它。 我相信代码来自 bcrist,因为它从他的一个 cmets 链接到 OP。【参考方案2】:

我相信您在优化器中发现了一个错误。您可以通过禁用优化或通过在最里面的for 循环中添加具有无法优化的副作用的额外代码(例如cout &lt;&lt; "hi")来获得发布版本以提供与调试版本相同(正确)的输出(这大概可以防止任何优化被错误地执行)。我建议向 Microsoft 报告。


更新:Microsoft confirms 这是一个与自动矢量化相关的错误,它已在 VS2013 更新 2 中修复。其他版本的解决方法是通过在循环前加上 #pragma loop(no_vector) 来禁用矢量化。

此外,他们还描述了两种可以触发错误的不同循环结构。我将引用它们:

有两种情况会出现错误:

1) 正如用户 burzvingion 所提到的,将 形式:

for (int i=0; ...) sum = A[...] - sum;

2) 形式矢量化的循环:

for (int i=0; ...) sum = sum + A[ - i];

他们还给出了以下定位易受攻击代码的建议:

如果您正在查看源代码以尝试找到这些 情况下,我建议先抛出 /Qvec-report:1 来查找所有 被矢量化的循环,并从那里开始。解决方法 错误,将 #pragma loop(no_vector) 放在 for 循环之上。

【讨论】:

我继续向 MS 提交了错误报告。当/如果他们回复,我会更新这个答案。 connect.microsoft.com/VisualStudio/feedback/details/893189/…【参考方案3】:

产生优化错误的代码可以简化为:

#include <iostream>
using namespace std;

#define SIZE 12

int main()

    int A[SIZE] = 0;

    int sum = 0;
    for (int i=0; i<SIZE; i++)
        sum += A[SIZE-1-i];
    cout << sum << endl;

    return 0;


可以通过应用任一以下更改来消除优化错误:

    SIZE的定义改为小于12的值 将表达式A[SIZE-1-i] 更改为A[SIZE-i-1] 将操作cout &lt;&lt; sum &lt;&lt; endl移动到循环中

所以为了诊断问题,我们可以简单地应用这些更改中的任何一个,然后比较更改前的代码反汇编和更改后的代码反汇编。

【讨论】:

#1 的解释可能只是优化器在少于 12 次迭代时展开循环。不过,我会对 #2 很好奇... @dlf:实际上我自己检查了差异,结果发现#1 和#2 非常相似。我没有注意到第一个更改中的循环展开,但如果有,那么它很可能也发生在第二个更改中(因此,它很可能没有发生在其中任何一个更改中)。【参考方案4】:

我比较了两种情况的 asm 代码(在 VC++ 2013 express 中),在发布版本中,for 循环的发布版本中的 asm 代码

for (int i = 0; i&lt; Period; i++)

在下面,和debug build中的很不一样

$LL6@main:

; 23   :        sum = 0;
; 24   :        for (int i = 0; i< Period; i++)

    xorps   xmm5, xmm5
    lea eax, DWORD PTR [edi+88]
    xorps   xmm4, xmm4
    mov ecx, 3
    npad    2
$LL3@main:

; 25   :            //cout << "hi";
; 26   :            sum += A[bars - i];

    movd    xmm2, DWORD PTR [eax-4]
    lea eax, DWORD PTR [eax-32]
    movd    xmm0, DWORD PTR [eax+32]
    movd    xmm1, DWORD PTR [eax+36]
    movd    xmm3, DWORD PTR [eax+40]
    punpckldq xmm3, xmm0
    movd    xmm0, DWORD PTR [eax+48]
    punpckldq xmm1, xmm2
    movd    xmm2, DWORD PTR [eax+44]
    punpckldq xmm3, xmm1
    movd    xmm1, DWORD PTR [eax+52]
    paddd   xmm5, xmm3
    movd    xmm3, DWORD PTR [eax+56]
    punpckldq xmm3, xmm0
    punpckldq xmm1, xmm2
    punpckldq xmm3, xmm1
    paddd   xmm4, xmm3
    dec ecx
    jne SHORT $LL3@main

; 23   :        sum = 0;
; 24   :        for (int i = 0; i< Period; i++)

    paddd   xmm4, xmm5
    xor edx, edx
    movdqa  xmm0, xmm4
    mov eax, edi
    psrldq  xmm0, 8
    mov esi, 3
    paddd   xmm4, xmm0
    movdqa  xmm0, xmm4
    psrldq  xmm0, 4
    paddd   xmm4, xmm0
    movd    ebx, xmm4
    npad    7
$LL30@main:

; 25   :            //cout << "hi";
; 26   :            sum += A[bars - i];

    add ecx, DWORD PTR [eax]
    lea eax, DWORD PTR [eax-8]
    add edx, DWORD PTR [eax+4]
    dec esi
    jne SHORT $LL30@main

; 27   :    


正如您可以从 asm 代码中看到的那样,此处使用 SSE 指令。所以我检查了compiler options VC++ 中的 SSE 指令,然后我指定 /arch:IA32 在发布版本中禁用 x86 处理器的 SSE 和 SSE2 指令生成,然后得到与调试版本相同的结果。

我对SSE不熟悉,希望有人可以根据我的发现进行更多解释。

【讨论】:

以上是关于Visual Studio 2012 不同的值发布/调试模式的主要内容,如果未能解决你的问题,请参考以下文章

从文件中读取数据并将其写入不同的文件(Visual Studio 2012)

部署 SSIS 包时 Visual Studio 2012 崩溃

Visual Studio 2012 空白解决方案消失

如何为 Visual Studio 2012 调试器编写自定义本机可视化工具 DLL?

是否可以使用 Visual Studio 调试器的 Watch 窗口查看内存位置的值?

Qt5 Visual Studio 2012 插件不允许调试到 QStack/QVector