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 将会是本篇文章的重点关注对象。

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;
}

::: 此时程序的正确输出应该为:190,190。但是由于编译器的某些 bug 导致在对加宽乘法的处理时产生错误的归约,两个变量的累加器被错误地合并。通过输出中间生成的汇编文件(详情在这里)我们可以看到甚至生成了 W 和 M 相加的代码。 :::info[详情] 当上述程序使用以下编译命令:

-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

:::

解决方案

这里可以停下来猜一猜,大致可以分为以下几种。如果你想直接知道所有解决办法及其性价比请看最后一个。

  1. 在循环中加入 cout<<"";。是的,输出一个空串。 用 printf 输出空串不可以哟。(可能带来不必要的 IO 开销)
  2. 声明一个 volatile 变量并在循环中修改它。 (可能带来不必要的性能开销)
  3. 既然是因为两个累加器混乱导致的,那我们就把一个循环拆为两个循环计算即可。(这是最好的方案)
  4. 去掉 -march=native 或 -O2 即可,但是洛谷上现在已经不能改编译选项了。(洛谷默认带 -march=native,且通常开 -O2,所以这个 bug 在洛谷上天然容易触发)
  5. 把上述的数组修改为其他类型避免此 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;
}

:::

这段代码通过一堆乱七八糟的位运算使得编译器误认为 else 分支永远不会执行。

触发条件

在 GCC 15.1.0 至 15.3.x 之中 ,优化级别 -O1 及以上。

来源

GCC Bugzilla #125444。

解决方案

感觉没什么比较巧妙通用的解决方案,主要目的是防止编译器判断表达式时错误。

如何避免

总之,尽量不要在 if 判断条件中放入过于复杂、容易被编译器过度优化的表达式。根据这个 bug 来看似乎是位运算导致的错误,所以写状压什么的时候稍作注意即可,感觉触发的概率不如我在 CSP-S 中拿到 401 分的好成绩。

速通本文

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

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

最后

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

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

会有第二期的喵。

附录 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 备注
#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 贡献。

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