GCC:你写的对,但我优化出问题了 —— 编译器部分常见 Bug 测试与规避(GCC 15.1.0)
起因
在开始之前,请先允许我做一个违背祖宗的决定:把锅甩给编译器。毕竟在 OI 圈混久了,你总会听到那句至理名言——“代码错了,先找自己的问题”。我本来也是这么想的,甚至把这句话刻进了 DNA,刻得比 #include <bits/stdc++.h> 还熟。直到 GCC 15.1.0 用 -O2 给我上了一课:有些 WA,真的不是你的错。我调了四个小时,能改的都改了,能怀疑的都怀疑了,连键盘都差点被我怀疑是内鬼,就是没怀疑编译器。结果呢?编译器它真的有问题!于是,这篇文章诞生了。它不教你如何写代码,只教你如何在编译器发疯时保命。如果你也曾在洛谷 IDE 上对着莫名其妙的 RE 怀疑人生,欢迎来到同一条贼船。船票拿好,锅也拿好,咱们挤挤喵。
这个帖子 以及 这个帖子 是最开始我遇到的 bug。
简要说明
本文具有时效性。
:::error[本文关于 luogu 编译器版本信息可能出现与现行信息不符] 本文写作日期为 2026.9.22,但是 luogu 已经在 2026.10.13 更换了 GCC 版本为 15.3.0。
| 现已经对部分影响文章正确性的信息做了修改,部分实测版本由于测试时版本为 15.1.0 不做修改。 |
|---|
本文涉及的 bug 中,多数在 Windows 实测可触发,具体环境见各节“触发条件”。 :::warning[本文讨论范围] 本文基于洛谷在线测评环境(GCC 15.3.0 / GCC 9.3.0)与 NOI Linux 2.0 相关环境,所有结论均为特定版本、特定编译选项下的实测结果。不同平台、不同 GCC 版本可能表现不同。本文不讨论由未定义行为、变量名冲突等非编译器 Bug 导致的问题。 ::: 本文按 GCC 版本分节介绍具体案例。如果你只想直接看规避建议,可以跳到正文末尾的“速通本文”。
详情请见洛谷帮助中心
顺带提一下 -march=native 和 -O2 将会是本篇文章的重点关注对象。
- 附录 A 为本文部分 Bug 涉及的 GCC 优化阶段
- 附录 B 为如何快速判断是否为编译器 Bug
- 附录 C 为文章各个 bug 复现所要求的优化级别表。
GCC 15.1.0
我们来找一个非常典型的 bug 详细讲解。
关于循环向量化中对加宽乘法的错误处理
影响版本:GCC 15.1.0 – 15.3.x
验证状态:GCC 15.1.0 实测触发;15.4+ 官方标注“已修复”。
bug 描述
我们可以发现有如下代码: :::info[code]
#include <stdio.h>
int L[5][2] = {{1,2},{3,4},{5,6},{7,8},{9,10}};
int R[5][2] = {{2,1},{4,3},{6,5},{8,7},{10,9}};
int main() {
int n = 5;
long long W = 0, M = 0;
for (int i = 0; i < n; i++) {
W += 1ll * L[i][1] * R[i][1];
M += 1ll * L[i][0] * R[i][0];
}
printf("%lld %lld\n", W, M);
return 0;
}
:::
此时程序的正确输出应该为:
-std=$version -fPIC -DONLINE_JUDGE -Wall -fno-asm -lm -march=native -O2
程序输出:
226 154
当上述程序使用以下编译命令:
-std=$version -fPIC -DONLINE_JUDGE -Wall -fno-asm -lm -march=native
或
-std=$version -fPIC -DONLINE_JUDGE -Wall -fno-asm -lm -O2
程序输出:
190 190
:::
解决方案
这里可以停下来猜一猜,大致可以分为以下几种。如果你想直接知道所有解决办法及其性价比请看最后一个。
- 在循环中加入
cout<<"";。是的,输出一个空串。 用printf输出空串不可以哟。(可能带来不必要的 IO 开销) - 声明一个
volatile变量并在循环中修改它。 (可能带来不必要的性能开销) - 既然是因为两个累加器混乱导致的,那我们就把一个循环拆为两个循环计算即可。(这是最好的方案)
- 去掉
-march=native或-O2即可,但是洛谷上现在已经不能改编译选项了。(洛谷默认带-march=native,且通常开-O2,所以这个 bug 在洛谷上天然容易触发) - 把上述的数组修改为其他类型避免此 bug,包括但不限于
char、short、long long、size_t、__int128、unsigned long long、unsigned char。但是请注意不包括unsigned int和unsigned这两个类型。据我测试,在64 位环境下,常见的非32 位整型都可以不触发此 bug。 另外,其实直接修改第二维度的大小也可以(修改为3 是一种可行方法)。
所有方案对比表:
| 方案 | 可靠性 | 性能开销 | 推荐度 |
|---|---|---|---|
| 拆循环 | 高 | 低 | 5 |
| 改数据类型/数组大小 | 高 | 视情况 | 4 |
| volatile | 中 | 中 | 2 |
| cout<<"" | 低 | 高 | 1 |
触发条件
洛谷 IDE 中使用(C++ 17),启用 -O2优化。
注意:经我测试,Windows 环境下此 bug 在 GCC 15.1.0 的各版本中均可复现(编译选项必须启用 -march=native 以及至少 -O2 的优化级别)。
来源
GCC Bugzilla #123002,其实这个 笛卡尔树的模板 中好多人遇到的就是此问题,并且 chen_zhe 的帖子也在描述此问题。
如何避免
不要在同一个循环内用不同的变量进行加宽乘法。
其他
经过我的随机抽样,我发现大部分 bug(除了本文章介绍的 bug)已经在现行 15.1.0 版本中被修复,另外的 bug 要么就是需要更不常见(对竞赛编程来说)的 bug,要么就是不像是可观测宇宙内能触发的 bug,这里再介绍一个出现概率极其小的 bug。
复杂位运算条件被错误消除
修复情况
截至文章写作时仍未修复。
描述
如果你感到下面这段代码冗长且晦涩难懂,我感到十分抱歉,请原谅我没有找到更短的复现代码。 :::info[code]
#include <stdio.h>
#include <stdint.h>
#define CHAR_BIT 8
/* 宏定义来自原始测试用例,用于模拟复杂的位操作 */
#define safe_unary_minus_func_int16_t_s(_si) \
({ int16_t si = (_si) ; \
(((int16_t)(si)) == (INT16_MIN)) ? \
((int16_t)(si)) : \
(-((int16_t)(si))); })
#define safe_lshift_func_uint16_t_u_u(_left, _right) \
({ uint16_t left = (_left); unsigned int right = (_right) ; \
((((unsigned int)(right)) >= sizeof(uint16_t) * CHAR_BIT) \
|| (((uint16_t)(left)) > ((UINT16_MAX) >> ((unsigned int)(right))))) ? \
((uint16_t)(left)) : \
(((uint16_t)(left)) << ((unsigned int)(right))); })
int idx = 0;
/* 这个函数被标记为 noinline,它的调用是一个可观察的副作用 */
void __attribute__((noinline)) marker_171() {
idx++;
}
int8_t *a;
uint16_t b;
int8_t **c(int8_t **f);
static uint8_t d(int8_t *const *, int8_t **, uint8_t);
uint8_t e() {
c(NULL);
return 0;
}
int8_t **c(int8_t **f) {
uint8_t aa = 3;
int8_t **g = &a;
int i;
for (i = 0; i < 10; i++) {
d(f, g, b ^= safe_unary_minus_func_int16_t_s(aa));
}
return g;
}
uint8_t d(int8_t *const *l, int8_t **j, uint8_t k) {
/* 这个复杂的条件表达式是触发 Bug 的核心 */
if (safe_lshift_func_uint16_t_u_u(0, k), k) {
;
} else {
marker_171(); /* 这个调用可能被错误地消除 */
}
return k;
}
int main() {
e();
printf("%d\n", idx);
return 0;
}
:::
- 在 GCC 15.1.0 至 15.3.x 上,程序会输出
0,这表示marker_171()被错误地忽略掉了。 - 在修复后的版本(GCC 15.4+ 或在本版本下使用 -O0)上,会输出
5。 - 由于
b 的值不停的在0 和65533 之间交替,导致每两次循环才调用一次marker_171()函数,由于循环了10 次,故正确答案为5 。
这段代码通过一堆乱七八糟的位运算使得编译器误认为 else 分支永远不会执行。
触发条件
在 GCC 15.1.0 至 15.3.x 之中 ,优化级别 -O1 及以上。
来源
GCC Bugzilla #125444。
解决方案
感觉没什么比较巧妙通用的解决方案,主要目的是防止编译器判断表达式时错误。
如何避免
总之,尽量不要在 if 判断条件中放入过于复杂、容易被编译器过度优化的表达式。根据这个 bug 来看似乎是位运算导致的错误,所以写状压什么的时候稍作注意即可,感觉触发的概率不如我在 CSP-S 中拿到 401 分的好成绩。
速通本文
以下是本文所有 Bug 的规避建议列表。如果你是从头到尾一路读下来的(我很感激),可以对照前文案例快速回顾;如果你是从开头跳转过来的,当然可以直接看这里。请注意以下 bug 的触发均需要启用一定级别的优化(详情见附录 C)。而且如果出现下述写法触发 bug 的概率仍然不高。
- 尽量不要在同一个循环内用不同变量做加宽乘法。
if条件中尽量避免过于复杂的位运算。
当然,编译器 bug 并没有那么好触发,如果你尝试进行了上述操作但是没有复现那当然是好事情喵。
最后
希望大家永远不会遇到编译器问题,祝大家所有比赛 rp++。如有错误欢迎大家指出。
本文所涉 Bug 基本附有官方链接,若你发现了新的触发条件、修复版本或未收录的 bug,欢迎在评论区或洛谷私信补充。
会有第二期的喵。
附录 A:本文部分 Bug 涉及的 GCC 优化阶段
- 加宽乘法归约:
tree-vectorization/vect归约处理
附录 B:如何快速判断是否为编译器 Bug
- 竞赛常见的 bug 部分与
-O2密切相关,请确保在-O2条件下与-O0不同(尽管许多 UB 在这一点上很符合该条)。 - 可以在编译时尝试加入以下命令
-Wall -Wpedantic -Wextra等用于检查 UB,-fsanitize=undefined可能因为文件缺失的问题不能使用。 - 尝试删减无关代码查看是否能够正确复现。
- 去对照一下有没有常见的 UB。
- 尝试更换编译器版本。
附录 C:优化触发级别
| Bug | GCC版本 | 平台 | -O0 | -O1 | -O2 | -O3 | -march=native |
备注 |
|---|---|---|---|---|---|---|---|---|
| #123002 | 15.1.0 | 洛谷 IDE 和本地 | 正常 | 正常 | 触发 | 触发 | 必需 | 加宽乘法 |
| #125444 | 15.1.0 | 洛谷 IDE 和本地 | 正常 | 触发 | 触发 | 触发 | 无影响 | 复杂位运算 |
注:123002 的 Bugzilla 标题并未提及 -march=native(根据一些零散的描述,该 bug 的报告者报告复现需要 -mavx2 -O3),它是该 Bug 的额外触发条件,但并非官方确认的必要条件。
AI 和人类使用说明
本文第一段、附录 A 以及“速通本文”章节由人类写作,用 deepseek V4 pro 进行润色。其余段由非 AI 独立完成并润色。
保证文章非生成式 AI 贡献远大于 AI 贡献。
保证人类贡献远大于非人类贡献。