GCC:你没写错,但是我优化出锅了(第二期 GCC 9.3.0)

· · 算法·理论

起因

首先,我要挑战一下编程界的绝对真理——“编译器永远是对的”。

相信大家都听过这句话。毕竟,编译器可是经过无数大佬千锤百炼的杰作,怎么可能出错呢?所以,当你的代码跑不对时,首先应该反思自己:是不是思路挂了?是不是又有 UB?是不是数组越界了?是不是昨晚没睡好?是不是忘了拜一拜机器了?

但是,如果你把代码翻来覆去检查了八百遍,确认没有 UB,结果程序还是在开启优化后开始表演玄学;如果你在考场上绞尽脑汁,死活想不明白为什么自己的代码突然背叛了自己——那么,恭喜你,你可能遇到了比题目本身更有挑战性的东西:编译器 Bug。

没错,编译器也会犯错。它不仅会犯错,有时候还会非常自信地把你的正确代码优化成错误答案,让你甚至怀疑是不是自己的脑子出了 Bug。

当然,这种事情并不常见,但也绝非都市传说。本文就来聊聊那些真实存在、经过实际测试的 GCC Bug,看看这位平时值得信赖的老伙计,究竟能整出多少让人血压升高的活。

如果你也遇到过类似的灵异事件,或者单纯想看看编译器能有多离谱,那么就请继续阅读吧。

luogu 在 2026.10.13 的时候更新了 GCC 版本到 15.3.0 之后再补一下这个版本的 bug 吧。

第一期 如果你想了解 GCC 15.1.0 的 bug 那么可以去看一下,现在 luogu 的编译器升级到了 15.3.0

这个帖子 以及 这个帖子 是最开始我遇到的 bug。

简要说明

本文具有时效性。

本文涉及的 bug 中,多数在 Windows 实测可触发,具体环境见各节“触发条件”。 :::warning[本文讨论范围] 本文基于洛谷在线测评环境(GCC 9.3.0)与 NOI Linux 2.0 相关环境,所有结论均为特定版本、特定编译选项下的实测结果。不同平台、不同 GCC 版本可能表现不同。本文不讨论由未定义行为、变量名冲突等非编译器 Bug 导致的问题。 ::: 本文按 GCC 版本分节介绍具体案例。如果你只想直接看规避建议,可以跳到正文末尾的“速通本文”。

详情请见洛谷帮助中心 :::info[这是一些关于洛谷在线测评的信息,这里将简单普及一下]

编译开关

# O2 优化开关为 -O2
gcc -x c -std=c99 -DONLINE_JUDGE -Wall -fno-asm -lm -march=native
g++ -x c++ -std=$version -fPIC -DONLINE_JUDGE -Wall -fno-asm -lm -march=native

当前的版本信息为:

# gcc --version
gcc (GCC) 15.3.0
Copyright (C) 2025 Free Software Foundation, Inc.

另外,针对 NOI 系列比赛(也是洛谷用户参赛比例最大的比赛)的环境,洛谷专门提供了 GCC 9.3.0 版本的 C++14 标准选项,以符合 CCF 公布的 NOI Linux 2.0 竞赛环境(官网)。这也是洛谷在大多数情况的默认选择的语言。它的编译开关和上面一样,当前版本信息如下:

# gcc --version
gcc (GCC) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.

::: 顺带提一下 -march=native 和 -O2 将会是本篇文章的重点关注对象。

GCC 9.3.0

起初以为第一篇 bug 没有出现在 GCC 9.3.0 中,差点让我感受到了此版本的先进。但深入调查后发现并非如此。

这应该就是大家比较关心的部分了,我尽可能的来展开这一部分。 :::warning[注意] 我不能保证在洛谷 IDE(GCC 9)上的运行结果与 NOI Linux 2.0 竞赛环境上的运行结果一致,因为我并没有实测。

另外,针对 NOI 系列比赛(也是洛谷用户参赛比例最大的比赛)的环境,我们专门提供了 GCC 9.3.0 版本的 C++14 标准选项,以符合 CCF 公布的 NOI Linux 2.0 竞赛环境(官网)。这也是洛谷在大多数情况的默认选择的语言。

——洛谷帮助中心

:::

不要用 __int128 做数组下标

描述

让我们来做一道“初赛题”。 :::info[Problem]

#include <cstdio>
__int128 idx=0;
int main() {
    int a[2] = {1, 2};
    idx++;
    a[idx] = 0;
    printf("%d %d", a[0], a[1]); 
}

上面这个代码在洛谷 IDE 中使用(C++14 (GCC 9))开启 -O2 优化的测试运行结果为()。

A. 1 2 B. 1 0 C. 0 0 D. 1 1
:::info[Answer]
答案:A
解析:在不开 -O2 优化的情况下选 B。

答案看起来相当反直觉。

触发条件

洛谷 IDE 中使用 C++14 (GCC 9),启用 -O2优化。

注意:经我测试,Windows 环境下此 bug 在 GCC 9.3.0 的各版本中均可复现(至少启用 -O1 优化)。

原因

__int128 idx 被用作数组 a 的下标。在 -O2 优化级别下,编译器在进行值范围分析时,需要计算 a[idx] 这个数组引用的最大可能范围。由于 __int128 的位宽(128 位)超出了 GCC 内部用于地址计算的 offset_int 类型所能安全表示的范围,编译器错误地截断了 idx 的值域信息,从而对 a[idx] = 0 这个操作进行了错误判断,最终导致该赋值被忽略了(我承认在查询资料时气笑了)。

来源

GCC Bugzilla 邮件列表存档(2026 年 1 月),感觉正常情况不会这么写,在 C++ 中连 y1 这样的名字都能作函数名,所以什么写法都可能出现。

如何避免

不要用 __int128 做下标就好了。(不知道有没有人真的这么写过喵)

关于 bool 变量用关系表达式赋值

这个 bug 相当诡异。

该 bug 的出现是由于用关系表达式赋值 bool 类型的变量导致的。错误发生在归纳变量优化阶段上,编译器尝试将其优化为更简化的中间表示时意外生成了形如 _2 = _4 - _21; 的代码——注意:这里 _2 和 _4 均为 bool 类型——GCC 内部中间表示中不允许直接对 bool 类型生成减法,但优化阶段错误地生成了这种表达式。

触发条件

洛谷 IDE 中使用(C++14 (GCC 9)),启用 -O2优化。

注意:经我测试,Windows 环境下此 bug 在 GCC 9.3.0 的各版本中均可复现(至少启用 -O2)。

如何避免

尽量不要用关系表达式去赋值 bool 变量,虽然这个 bug 并不好触发。

无符号循环变量的风险

:::warning[更详细的界定] 该风险特指 GCC 9 系列在 -O1 及以上优化级别下,当循环涉及多个不同步长递增的局部无符号变量,并在循环体内进行无符号比较时,可能因 niter 分析缺陷导致循环被错误优化。 :::

描述

再来做一道初赛题目。 :::info[Problem] 这是一道判断题。

#include<iostream>
int main() {
    for (unsigned a=0,b=0; a < 6; a += 1, b += 2) {
        if (b < a) return 1;
    }
    return 0;
}

上面这个代码在洛谷 IDE 中使用(C++14 (GCC 9))开启 -O2 优化的测试不会触发 RE。()

注意:洛谷将非零返回码视为 RE。
:::info[Answer]
错误。
:::

这个 bug 的原因在于 GCC 9 的循环迭代次数分析在处理无符号数回绕时存在缺陷。这使得编译器为 if 语句中的条件会最终为真,执行 return 1 导致程序 RE。

触发条件

洛谷 IDE 中使用 C++14 (GCC 9),启用 -O1 及以上优化。

注意:经我测试,Windows 环境下此 bug 在 GCC 9.3.0 的各版本中均可复现(至少启用 -O1)。

来源

GCC Bugzilla #102673,GCC Bugzilla #102666,这两个 bug 本质相同。

解决方案

可以仿照第一个 bug 的解决方案进行尝试,就像:

中间输出

实际上并不能解决此 bug,但是会发生有趣的事情。

#include<iostream>
int main() {
    for (unsigned int a=0,b=0; a < 6; a += 1, b += 2) {
        std::cout<<a<<" "<<b<<"\n";
        if (b < a) return 1;
    }
    return 0;
}

运行后输出为:

0 0
1 2
2 4
3 6

在输出后会被判断为 RE。可以发现在输出中没有任何一行中第二元素的值小于第一个元素的值,这个结果相当矛盾。

不用无符号变量

是的,避免在循环中使用无符号变量进行比较即可。

定义为全局变量

全局变量可能被其他翻译单元修改,编译器难以假设其值,因此不会进行过于激进的优化。

size_t 类型下的附加现象

如果在 64 位环境下我们把两个变量的类型改为 size_t 类型。那么我们就可以发现它在不停地输出根本轮不到 return 1 语句的执行(准确来说此语句被优化掉了,请继续阅读)。

如果使用 g++.exe -O2 -std=c++14 -S cs.cpp -o cs.s 导出中间汇编文件可以发现其行为等价于以下代码:

#include<iostream>
int main(){
    std::cout<<0LL<<" "<<0LL<<"\n";
    for(unsigned long long i=0;;++i){
        std::cout<<i<<" "<<i*2<<"\n";
    }
    return 0;
}

:::info[cs.s 中的明显错误部分]

.L4:
    addq    $1, %r12 //++i
    ...
    leaq    (%r12,%r12), %rdx//计算 2 * i,结果存入 %rdx
    ...
    jmp     .L4//无条件跳回.L4

::: 完整的 cs.s 文件请见附录 D。

在上述 size_t 版本的循环变量代码中,常见的 64 位无符号整型均会触发此 bug。

速通本文

以下是本文所有 Bug 的规避建议列表。如果你是从头到尾一路读下来的(我很感激),可以对照前文案例快速回顾;如果你是从开头跳转过来的,当然可以直接看这里。请注意以下 bug 的触发均需要启用一定级别的优化(详情见附录 C)。而且如果出现下述写法触发 bug 的概率仍然不高。

当然,编译器 bug 并没有那么好触发,如果你尝试进行了上述操作但是没有复现那当然是好事情喵。

最后

希望大家永远不会遇到编译器问题,祝大家所有比赛 rp++。如有错误欢迎大家指出。

本文所涉 Bug 基本附有官方链接,若你发现了新的触发条件、修复版本或未收录的 bug,欢迎在评论区或洛谷私信补充。

会有第三期(GCC 15.3.0)的喵。

附录 A:本文部分 Bug 涉及的 GCC 优化阶段

附录 B:如何快速判断是否为编译器 Bug

  1. 竞赛常见的 bug 部分与 -O2 密切相关,请确保在 -O2 条件下与 -O0 不同(尽管许多 UB 在这一点上很符合该条)。
  2. 可以在编译时尝试加入以下命令 -Wall -Wpedantic -Wextra 等用于检查 UB,-fsanitize=undefined 可能因为文件缺失的问题不能使用。
  3. 尝试删减无关代码查看是否能够正确复现。
  4. 去对照一下有没有常见的 UB。
  5. 尝试更换编译器版本。

附录 C:优化触发级别

Bug GCC版本 平台 -O0 -O1 -O2 -O3 -march=native 备注
__int128 下标 9.3.0 洛谷 IDE 和本地 正常 触发 触发 触发 未测 邮件列表,无编号
#121952 9.3.0 洛谷 IDE 和本地 正常 正常 触发 触发 未测 bool 关系赋值
#102666/#102673 9.3.0 洛谷 IDE 和本地 正常 触发 触发 触发 未测 无符号循环

注:123002 的 Bugzilla 标题并未提及 -march=native(根据一些零散的描述,该 bug 的报告者报告复现需要 -mavx2 -O3),它是该 Bug 的额外触发条件,但并非官方确认的必要条件。

附录 D:cs.s 文件

:::info[完整的 cs.s]

    .file   "cs.cpp"
    .text
    .p2align 4
    .def    __tcf_0;    .scl    3;  .type   32; .endef
    .seh_proc   __tcf_0
__tcf_0:
.LFB2077:
    .seh_endprologue
    leaq    _ZStL8__ioinit(%rip), %rcx
    jmp _ZNSt8ios_base4InitD1Ev
    .seh_endproc
    .def    __main; .scl    2;  .type   32; .endef
    .section .rdata,"dr"
.LC0:
    .ascii " \0"
.LC1:
    .ascii "\12\0"
    .section    .text.startup,"x"
    .p2align 4
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
.LFB1592:
    pushq   %r13
    .seh_pushreg    %r13
    pushq   %r12
    .seh_pushreg    %r12
    pushq   %rsi
    .seh_pushreg    %rsi
    pushq   %rbx
    .seh_pushreg    %rbx
    subq    $56, %rsp
    .seh_stackalloc 56
    .seh_endprologue
    leaq    .LC0(%rip), %rsi
    leaq    .LC1(%rip), %rbx
    call    __main
    movq    .refptr._ZSt4cout(%rip), %rcx
    xorl    %edx, %edx
    call    _ZNSo9_M_insertIyEERSoT_
    movl    $1, %r8d
    leaq    .LC0(%rip), %rdx
    movq    %rax, %r12
    movq    %rax, %rcx
    call    _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_x
    movq    %r12, %rcx
    xorl    %edx, %edx
    xorl    %r12d, %r12d
    call    _ZNSo9_M_insertIyEERSoT_
    leaq    .LC1(%rip), %rdx
    movq    %rax, %rcx
    call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
    movq    .refptr._ZSt4cout(%rip), %rax
    movq    %rax, 40(%rsp)
.L4:
    movq    40(%rsp), %rcx
    addq    $1, %r12
    movq    %r12, %rdx
    call    _ZNSo9_M_insertIyEERSoT_
    movl    $1, %r8d
    movq    %rsi, %rdx
    movq    %rax, %r13
    movq    %rax, %rcx
    call    _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_x
    leaq    (%r12,%r12), %rdx
    movq    %r13, %rcx
    call    _ZNSo9_M_insertIyEERSoT_
    movq    %rbx, %rdx
    movq    %rax, %rcx
    call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
    jmp .L4
    .seh_endproc
    .p2align 4
    .def    _GLOBAL__sub_I_main;    .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_main
_GLOBAL__sub_I_main:
.LFB2078:
    subq    $40, %rsp
    .seh_stackalloc 40
    .seh_endprologue
    leaq    _ZStL8__ioinit(%rip), %rcx
    call    _ZNSt8ios_base4InitC1Ev
    leaq    __tcf_0(%rip), %rcx
    addq    $40, %rsp
    jmp atexit
    .seh_endproc
    .section    .ctors,"w"
    .align 8
    .quad   _GLOBAL__sub_I_main
.lcomm _ZStL8__ioinit,1,1
    .ident  "GCC: (MinGW-W64 x86_64-posix-seh, built by Brecht Sanders) 9.3.0"
    .def    _ZNSt8ios_base4InitD1Ev;    .scl    2;  .type   32; .endef
    .def    _ZNSo9_M_insertIyEERSoT_;   .scl    2;  .type   32; .endef
    .def    _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_x;  .scl    2;  .type   32; .endef
    .def    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc;    .scl    2;  .type   32; .endef
    .def    _ZNSt8ios_base4InitC1Ev;    .scl    2;  .type   32; .endef
    .def    atexit; .scl    2;  .type   32; .endef
    .section    .rdata$.refptr._ZSt4cout, "dr"
    .globl  .refptr._ZSt4cout
    .linkonce   discard
.refptr._ZSt4cout:
    .quad   _ZSt4cout

:::

AI 和人类使用说明

本文第一段、附录 A 以及“速通本文”章节由人类写作,用 deepseek V4 pro 进行润色。其余段由非 AI 独立完成并润色。

保证文章非生成式 AI 贡献远大于 AI 贡献。

保证人类贡献远大于非人类贡献。