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 将会是本篇文章的重点关注对象。
- 附录 A 为本文部分 Bug 涉及的 GCC 优化阶段
- 附录 B 为如何快速判断是否为编译器 Bug
- 附录 C 为文章各个 bug 复现所要求的优化级别表。
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 的位宽(a[idx] = 0 这个操作进行了错误判断,最终导致该赋值被忽略了(我承认在查询资料时气笑了)。
来源
GCC Bugzilla 邮件列表存档(2026 年 1 月),感觉正常情况不会这么写,在 C++ 中连 y1 这样的名字都能作函数名,所以什么写法都可能出现。
如何避免
不要用 __int128 做下标就好了。(不知道有没有人真的这么写过喵)
关于 bool 变量用关系表达式赋值
这个 bug 相当诡异。
-
官方状态:RESOLVED FIXED。
-
影响版本:GCC 9 regression。
-
官方报告限定 x86-64 Linux;实测 MinGW-w64 GCC 9.3.0(Windows x86-64)也可触发。
来源
GCC Bugzilla #121952,先声明一下,接下来据我测试与其描述有出入。
描述
:::info[代码] 代码中为我的测试结果。
#include <cstdio> #include<iostream> using namespace std; int main() { bool ac[19];//这里关于数组 ac 的大小据我测试有一些小的数据在标准输入内输入 1 可以触发该 bug,但该 bug 报告者描述必须大于等于 19。详情请见下面的折叠框。 long long n; cin>>n;// 输入 1 for (int i = 1; i <= n; i++) ac[i] = (((n - i + 1) % 2) == 0); const bool exp = (((n - n + 1) % 2) == 0); // 当 n=1 时,expected = false cout<<ac[n]<<"\n"<<exp<<"\n"; if (ac[n] != exp) { printf("BUG FOUND!!\n"); return 1; } return 0; }::: :::warning[在 IDE 上测试与 bug 提供者描述不符] 该 bug 报告中描述道:
bool ac[19]; // must be >= 19 for bug to be seen但是据我测试,在标准输入为 1的情况下,这里ac 数组的大小还可以是集合\{6,8,10,12,14,16,17,18\} (尽管0 长数组是非标准的,但0 仍然可以触发该 bug)中的元素(同样包括代码中的描述),更加诡异的是这里的变量n 的类型必须被声明为long long等常见64 位整型(size_t的位宽取决于机器)(尽管该 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 类型下的附加现象
如果在 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 的概率仍然不高。
- 尽量不要在同一个循环内用不同变量做加宽乘法。
- 尽量不要用
__int128做数组下标。 - 尽量不要用关系表达式给
bool数组元素赋值。 - 尽量避免多个不同步长的无符号循环变量。
if条件中尽量避免过于复杂的位运算。
当然,编译器 bug 并没有那么好触发,如果你尝试进行了上述操作但是没有复现那当然是好事情喵。
最后
希望大家永远不会遇到编译器问题,祝大家所有比赛 rp++。如有错误欢迎大家指出。
本文所涉 Bug 基本附有官方链接,若你发现了新的触发条件、修复版本或未收录的 bug,欢迎在评论区或洛谷私信补充。
会有第三期(GCC 15.3.0)的喵。
附录 A:本文部分 Bug 涉及的 GCC 优化阶段
__int128下标:tree-optimization值范围分析、offset_int截断bool关系赋值:IV-OPTS归纳变量优化- 无符号循环变量:
niter analysis循环迭代次数分析
附录 B:如何快速判断是否为编译器 Bug
- 竞赛常见的 bug 部分与
-O2密切相关,请确保在-O2条件下与-O0不同(尽管许多 UB 在这一点上很符合该条)。 - 可以在编译时尝试加入以下命令
-Wall -Wpedantic -Wextra等用于检查 UB,-fsanitize=undefined可能因为文件缺失的问题不能使用。 - 尝试删减无关代码查看是否能够正确复现。
- 去对照一下有没有常见的 UB。
- 尝试更换编译器版本。
附录 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 贡献。
保证人类贡献远大于非人类贡献。