为什么在发布模式下短整型的模运算不正确?

6
短整数模数不正确。这真的很奇怪,已经让我浪费了两天时间。我已经尽可能简化了代码并缩小了问题范围,如下所示:
#include <stdio.h>
#include <stdlib.h>

int foo(short Width, short Height, short MSize) 
{
    short i = 0, k = 0, pos = 0;
    short j = 0;

    for(j = 1; j < Width - 1; j = j + 1)
    {/* a blank loop */}

    for(i = 1; i < Height - 1; i = i + 1) {
        for(j = 1; j < Width - 1; j = j + 1) {
            if((j % MSize) == 0) {
                k = k + 1;
            }
            printf("i=%d, k=%d, j=%d, MSize=%d, j mod MSize=%d\n", (int)i, (int)k, (int)j, (int)MSize, (int)(j % MSize));
            if (pos >= 1024) {
                fprintf(stderr, "pos = %d, over 1024\n", (int)pos);
            }
            pos = pos + 1;
        }
    }
    return 0;
}

int main(int argc, char* argv[])
{
    foo(32, 32, 8);
    return 0;
}

在 Debug 模式下编译,以上代码可以正常运行,j%MSize 的结果是正确的,但是在 Release 模式下编译,j%MSize 的结果始终为 7,这是无意义的(在 Visual Studio 2005/2012/2013 中测试过)。由于没有内存操作,所以不应该是由栈破坏引起的。 有人知道原因吗?
我看到的输出是(稍微编辑了一下):
j=10, MSize=8, j mod MSize=7
j=11, MSize=8, j mod MSize=7
j=12, MSize=8, j mod MSize=7
j=13, MSize=8, j mod MSize=7
j=14, MSize=8, j mod MSize=7
j=15, MSize=8, j mod MSize=7
j=16, MSize=8, j mod MSize=7
j=17, MSize=8, j mod MSize=7
j=18, MSize=8, j mod MSize=7
j=19, MSize=8, j mod MSize=7
j=20, MSize=8, j mod MSize=7
j=21, MSize=8, j mod MSize=7
j=22, MSize=8, j mod MSize=7
j=23, MSize=8, j mod MSize=7
j=24, MSize=8, j mod MSize=7
j=25, MSize=8, j mod MSize=7
j=26, MSize=8, j mod MSize=7
j=27, MSize=8, j mod MSize=7

以下是构建日志:
 1>Project "E:\Code\workspace\C\GeneralC\SNDFeatureExtract\SNDFeatureExtract.vcxproj" on node 2 (Build target(s)).

 1>ClCompile:

     D:\Program Files\Microsoft Visual Studio 11.0\VC\bin\CL.exe /c /Zi /nologo /W3 /WX- /sdl /O2 /Oi /Oy- /GL /D WIN32 /D NDEBUG /D _CONSOLE /D _MBCS /Gm- /EHsc /MT /GS /Gy /fp:precise /Zc:wchar_t /Zc:forScope /Fo"Release\\" /Fd"Release\vc110.pdb" /Gd /TP /analyze- /errorReport:prompt WeirdBug.cpp

     WeirdBug.cpp

   Link:

     D:\Program Files\Microsoft Visual Studio 11.0\VC\bin\link.exe /ERRORREPORT:PROMPT /OUT:"E:\Code\workspace\C\GeneralC\Release\SNDFeatureExtract.exe" /INCREMENTAL:NO /NOLOGO kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /manifest:embed /DEBUG /PDB:"E:\Code\workspace\C\GeneralC\Release\SNDFeatureExtract.pdb" /SUBSYSTEM:CONSOLE /OPT:REF /OPT:ICF /LTCG /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:"E:\Code\workspace\C\GeneralC\Release\SNDFeatureExtract.lib" /MACHINE:X86 /SAFESEH Release\WeirdBug.obj

     Generating code

     Finished generating code

     SNDFeatureExtract.vcxproj -> E:\Code\workspace\C\GeneralC\Release\SNDFeatureExtract.exe

 1>Done Building Project "E:\Code\workspace\C\GeneralC\SNDFeatureExtract\SNDFeatureExtract.vcxproj" (Build target(s)).

以下是来自 VS 的反汇编结果:
    short i = 0, k = 0, pos = 0;
    short j = 0;

    for(j = 1; j < Width - 1; j = j + 1)
00801014  mov         edi,1FF983C8h  
00801019  jl          foo+12h (0801012h)  
    {/* a blank loop */}

    for(i = 1; i < Height - 1; i = i + 1) {
0080101B  mov         edx,1  
00801020  mov         dword ptr [ebp-4],1  
00801027  mov         dword ptr [ebp-8],edx  
0080102A  and         ecx,80000007h  
00801030  jns         foo+37h (0801037h)  
00801032  dec         ecx  
00801033  or          ecx,0FFFFFFF8h  
00801036  inc         ecx  
00801037  mov         dword ptr [ebp-0Ch],ecx  
0080103A  lea         ebx,[ebx]  
00801040  mov         eax,1  
        for(j = 1; j < Width - 1; j = j + 1) {
00801045  mov         ebx,eax  
            if((j % MSize) == 0) {
00801047  test        ecx,ecx  
00801049  jne         foo+4Ch (080104Ch)  
                k = k + 1;
0080104B  inc         edi  
            }
            printf_s("i=%d, k=%d, j=%d, MSize=%d, j mod MSize=%d\n", (int)i, (int)k, (int)j, (int)MSize, (int)(j % MSize));
0080104C  push        ecx  
0080104D  push        8  
0080104F  push        eax  
00801050  movsx       eax,di  
00801053  push        eax  
00801054  push        edx  
00801055  push        80CD30h  
0080105A  call        printf_s (0801266h)  
            if (pos >= 1024) {
0080105F  mov         eax,400h  
00801064  add         esp,18h  
00801067  cmp         si,ax  
0080106A  jl          foo+86h (0801086h)  
                fprintf_s(stderr, "pos = %d, over 1024\n", (int)pos);
0080106C  movsx       eax,si  
                fprintf_s(stderr, "pos = %d, over 1024\n", (int)pos);
0080106F  push        eax  
00801070  push        80CD5Ch  
00801075  call        __iob_func (0801175h)  
0080107A  add         eax,40h  
0080107D  push        eax  
0080107E  call        fprintf_s (080127Ch)  
00801083  add         esp,0Ch  
        for(j = 1; j < Width - 1; j = j + 1) {
00801086  mov         ecx,dword ptr [ebp-0Ch]  
00801089  mov         edx,dword ptr [ebp-8]  
            }
            pos = pos + 1;
0080108C  inc         ebx  
0080108D  movsx       eax,bx  
00801090  inc         esi  
00801091  cmp         eax,1Fh  
00801094  jl          foo+47h (0801047h)  
    {/* a blank loop */}

    for(i = 1; i < Height - 1; i = i + 1) {
00801096  mov         eax,dword ptr [ebp-4]  
00801099  inc         eax  
0080109A  movsx       edx,ax  
0080109D  mov         dword ptr [ebp-4],eax  
008010A0  mov         dword ptr [ebp-8],edx  
008010A3  cmp         edx,1Fh  
008010A6  jl          foo+40h (0801040h)  
        }
    }
    return 0;
008010A8  pop         edi  
008010A9  pop         esi  
008010AA  xor         eax,eax  
008010AC  pop         ebx  
}
008010AD  mov         esp,ebp  
008010AF  pop         ebp  
008010B0  ret  

3
你能否提供一个更简洁的例子?{/* a blank loop */}真的必要吗? - this
2
@AliKazmi:仔细阅读核心代码-它们已经被正确初始化。 - lisyarus
1
无法重现。 - n. m.
3
我已在Microsoft Connect上提交了一个错误报告:https://connect.microsoft.com/VisualStudio/feedback/details/845052/optimization-bug-with-short-data-types-loops-and-whole-program-optimzation-gl。 - Michael Burr
1
@MatthieuM.:我在Connect bug上发布了一个稍微小一点的附件;它删除了大约10行和4个变量。似乎从那种形式做出任何小的改变都会导致问题消失。 - Michael Burr
显示剩余22条评论
3个回答

8

这是由于编译器的优化,与您的空循环有关。但我不太确定问题出在哪里。

为了简单解决这个问题,请将j声明为:

  volatile short j;

它会正常运行。因为程序每次从内存中获取j,而不是从寄存器中获取。

我调试了汇编代码,并发现程序在空循环后计算j % MSize并将其存储到内存中,每次在执行printf之前,只是从内存中获取值,而不是重新计算。

mov         ecx,dword ptr [ebp-10h] // j % MSize    @ memory
push        ecx  // j % MSize
mov         ecx,dword ptr [ebp-0Ch]  
push        8  // MSize
push        eax  // j
movsx       eax,word ptr [IdxY]  
movsx       esi,di  
push        esi  // k
push        eax  // IdxY
push        ecx  // i
// push static string and calling printf

但是如果添加一个volatile,它将会像这样:
mov         dx,word ptr [j]  
movsx       eax,dx  // j
and         eax,80000007h  // j % 8
push        eax 
// push other vars and calling printf

这里重新计算了MOD的值,并将其推入堆栈以供printf使用。因此很可能是编译器的一个错误,即使没有易失性添加,它也应该从内存中获取j的值。

由于我现在无法再添加评论 :(..我发现这是/Oxxx和/GL标志的问题。它会从以下选项中选择一个:

/O1 /O2 /Ox

为了查看问题,必须选择上述选项之一并使用/GL。

我的集成开发环境是Visual Studio 2010 10.0.40219.1 SP1Rel。


3
如果您不确定问题出在哪里,为什么认为空循环是导致问题的原因?而添加“volatile”只是一个权宜之计,简单明了,并不是解决方案 :-) - paxdiablo
运行得很好!所以,进一步的问题是:我应该在什么时候声明一个变量,以避免这种问题? - Jedi
1
@paxdiablo 问题的作者在评论中说,当空循环被移除时,问题不会出现,因此这个假设可能是正确的。然而,volatile 绝对不是解决问题的方法,一个空循环没有任何意义,如果你用有用的代码填充它,bug 也可能会消失。 - Excelcius
1
@Jedi 它通常用于多线程程序中,以避免变量的“意外”更改。但在这里发生这种情况很奇怪。正如paxdiablo所说,这不是最终解决方案,只是解决问题的一个巧妙方式。 - Jim Yang
2
迄今为止,根据必要条件,我认为错误是由公共子表达式消除和代码移动的组合引起的。 "重新加载值" 是真正的公共子表达式的正确优化,但这当然需要先计算和存储该CSE。优化器需要找到一个位置来存储它。看起来CSE计算被错误地提升出循环,仅当它之前有一个空循环时。 - MSalters
显示剩余9条评论

2
我看不出有任何问题。
$ gcc modulus.c 
$ ./a.out 
Width = 32, Height = 32, MSize = 8, Dim =16, sizeof(short)=2
i=1, IdxY=0, k=0, j=1, MSize=8, j mod MSize=1
i=1, IdxY=0, k=0, j=2, MSize=8, j mod MSize=2
i=1, IdxY=0, k=0, j=3, MSize=8, j mod MSize=3
i=1, IdxY=0, k=0, j=4, MSize=8, j mod MSize=4
i=1, IdxY=0, k=0, j=5, MSize=8, j mod MSize=5
i=1, IdxY=0, k=0, j=6, MSize=8, j mod MSize=6
i=1, IdxY=0, k=0, j=7, MSize=8, j mod MSize=7
i=1, IdxY=0, k=1, j=8, MSize=8, j mod MSize=0

我有所遗漏吗?


5
如果这是编译器的错误,它很可能不会发生在gcc中。 OP正在使用VS。 - Excelcius
@Excelcius:另一方面,至少我们知道程序是正确的机会,因为使用另一个编译器得到了预期的输出 :) - Matthieu M.
@MatthieuM。你说得完全正确,我并不是想让这个答案看起来是错误的,只是它在提供解决方案方面帮助不大。但它确实可以帮助其他人找到解决方案 :) - Excelcius

1
除了已经提供的其他答案,我想指出你可以通过更好的作用域来防止这样的错误(虽然在这种情况下不是你的错,很可能是编译器的错误)。
最好在循环范围内声明迭代变量。甚至更一般地说,只在使用它们的范围内声明变量。
如果你将第二个for-loop修改为以下内容:
for(short j = 1; j < Width - 1; j = j + 1) {

为了让jfor循环的作用域中被声明,编译器必须将j视为一个新变量,与之前的空循环无关。因此,它不太可能通过重复使用先前的内存位置来进行过度优化。这个小改变修复了VS2013中的错误,并且我认为它比使用volatile更加简洁。

是的,我同意你在C++风格方面的观点。但是在C风格中这是行不通的。 - Jedi
@Jedi 抱歉,我没有注意到C标签。虽然我知道这可能不是你想要的,但你仍然可以为第一个和第二个循环声明两个单独的变量。 - Excelcius

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接