Сompiler 错误?假设变量不变

Сompiler bug? Variable assumed unchanged

提问人:void 提问时间:11/10/2023 最后编辑:void 更新时间:11/10/2023 访问量:303

问:

Visual Studio 版本:17.7.1 (MSVC 19.37.32822)
使用默认设置和编译器标志新创建的项目。

最小可重复示例:

#include <cstdio>

__declspec(noinline) void test2(char** data)
{
    // After moving the pointer:
    // data_1 now points to data[1] = 1
    // data_0 now points to data[0] = 2
    *data += 1;
}

__declspec(noinline) void test(char* data_1)
{
    char* data_0 = data_1;
    test2(&data_1);
    int len = (int)(data_1 - data_0);

    if (*data_1 & 1)
    {
        if (*data_0 & 2)
            printf("good\n");
     }
}

int main()
{
    char data[2];
    data[0] = 2;
    data[1] = 1;
    test(data);
    return 0;
}

Release| x64 配置中没有打印“good”行。
调试或发布 |x86 产生预期结果。

我做了一些实验并查看了生成的汇编,以便将原始代码简化为这个 MRE。
问题的根本原因似乎是编译器的假设,该假设在 中保持不变。因此,可以省略该行,并可以使用该行代替在表达式中。
每一行都是必要的,包括 to int 的强制转换,这可能解释了 x86 构建中缺乏错误重现的原因。
data1test2()data_0 = data_1data_1data_0(*data_0 & 2)(data_1 - data_0)

更新 1

问题仅在 x64 发布版本中发生。通过将 /O2 更改为 /Od 或使用“修复”来禁用优化。#pragma optimize("", off)

VS 版本细分的可重现性:

  • 17.7.1 - 问题
  • 17.7.2 - 问题
  • ???
  • 17.7.6 - 没问题
  • 17.8 预览版 7 - 没问题

问题可能已经解决,或者其复制条件已更改。

更新 2

一位 MSVC 开发人员在私人谈话中证实了这个问题,我提出了这个问题来跟踪进度: https://developercommunity.visualstudio.com/t/Compiler-optimization-bug-in-VS-2022/10512534

C 可视化 C++ 编译器错误

评论

0赞 Daniel A. White 11/10/2023
您不会更改 test2 中的数据。您正在递增指针
1赞 Codeguard 11/10/2023
“您没有更改 test2 中的数据。你正在递增指针“——这是有意为之的。移动指针后,现在指向 ,而指向data_1data[1] = 1data_0data[0] = 2
0赞 Codeguard 11/10/2023
对于以前的评论者,请在下结论之前解释 Debug 和 Release 版本之间的区别。
2赞 void 11/10/2023
@ThomasMatthews 他们被要求解释该特定代码段的调试版本和发布版本之间的行为差异
1赞 bolov 11/10/2023
我没有看到任何UB的来源。在我看来,这种行为应该得到很好的定义。

答:

5赞 Martin Brown 11/10/2023 #1

我可以确认这里存在相同的行为。此外,任何尝试打印值以检查 else(失败)条件都会导致正确的行为。Optimiser 太激进了。它只获取 *data_1 并对其应用两个测试(假设 data_1==data_0这当然不是真的)。

对打印出调试值的任何更改似乎都会导致正确的行为。我将printf(“bad data_0”)添加到内部else子句中,可以预见的是,它被打印出来了。我更喜欢对所有路径进行注释。

FWIW Intel 编译器 2023 运行良好并打印“良好”。

冒烟枪 - MSC 编译器错误过于激进的优化无法加载 *data_0,并将这两个测试应用于 *data_1。拆卸在这里(略有添加,不影响错误行为):

--- C:\Users\Martin\source\repos\Toy_bug1\Toy_bug1.cpp ------------------------

// After moving the pointer:
// data_1 now points to data[1] = 1
// data_0 now points to data[0] = 2
*data += 1;
00007FF6D10B1070  inc         qword ptr [rcx]  
}
00007FF6D10B1073  ret  
[snip]
__declspec(noinline) void test(char* data_1)
{
00007FF6D10B1080  mov         qword ptr [rsp+8],rcx  
00007FF6D10B1085  sub         rsp,28h  
char* data_0 = data_1;
test2(&data_1);
00007FF6D10B1089  lea         rcx,[data_1]  
00007FF6D10B108E  call        test2 (07FF6D10B1070h)  
int len = (int)(data_1 - data_0);

if (*data_1 & 1)
00007FF6D10B1093  mov         rax,qword ptr [data_1]  
00007FF6D10B1098  movzx       ecx,byte ptr [rax]  
00007FF6D10B109B  test        cl,1  
00007FF6D10B109E  je          test+3Eh (07FF6D10B10BEh)  
{
    if (*data_0 & 2)
00007FF6D10B10A0  test        cl,2  // this test is incorrect!
        printf("good\n");
    else
        printf("bad d0");
}
00007FF6D10B10A3  lea         rax,[string "bad d0" (07FF6D10B2258h)]  
00007FF6D10B10AA  lea         rcx,[string "good\n" (07FF6D10B2250h)]  
00007FF6D10B10B1  cmove       rcx,rax  
}

我仍然对 MRE 中关键线的重要性感到有些困惑,该线已优化不存在,但对于显示故障至关重要! 即:

int len = (int)(data_1 - data_0);

如果没有此行,MSC 编译器将在 x64 版本中正确处理它。以下是在这种情况下生成的正确代码的反汇编:

//    int len = (int)(data_1 - data_0);

if (*data_1 & 1)
00007FF6D6D11096  mov         rax,qword ptr [data_1]  
00007FF6D6D1109B  test        byte ptr [rax],1  
00007FF6D6D1109E  je          test+3Eh (07FF6D6D110BEh)  
{
     if (*data_0 & 2)
00007FF6D6D110A0  test        byte ptr [rdx],2  
// rax  is data_1
// rdx  is data_0

故事的寓意是当心做一些混淆优化编译器的事情!

评论

1赞 void 11/10/2023
该行来自原始代码,其中 len 实际上是在后面使用的。所以我不是故意混淆编译器:)int len = (int)(data_1 - data_0);
0赞 Codeguard 11/10/2023
原始代码来自 mysql(特别是 mariadb): github.com/MariaDB/server/blob/11.3/storage/myisam/...在这里,第 797、802、824、839 行是相关的。请参见变量和 。startkeypos
0赞 Martin Brown 11/10/2023
奇怪的是,一条被优化到被遗忘的线条会产生如此烦人的副作用。我实际上想到了将 test2 更改为一个接受指针并在递增后返回其值的函数。我认为这足以使代码可靠,即使是最骨子里最激进的优化者。允许它内联也可能解决指针修改的可见性问题。您可以从 MSVC shell 内部运行英特尔编译器,并在它们之间切换。我觉得这很有帮助。英特尔在矢量化数字代码方面做得更好,并且更符合标准。
0赞 void 11/10/2023
将此标记为答案,因为您实际上费心去调查问题,而不是声称这是我的错误。问题已确认,MSVC 团队正在修复。
0赞 Codeguard 11/11/2023
这只是一个错误。编译器毕竟也是程序。我不认为“防御性”地编写代码或用一些额外的“坚持”来对抗编译器来膨胀它没有意义。错误将被修复,仅此而已。