难以理解的Bug #10:损坏的Windows构建

Lobsters Hottest 新闻

摘要

一篇博客文章,讲述了为开源量子计算库'stim'调试损坏的Windows构建的噩梦,强调了跨平台Python包构建和依赖管理的复杂性。

<p><a href="https://lobste.rs/s/rzujud/unfathomable_bugs_10_broken_windows">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/29 00:21

# 深不可测的 Bug #10:损坏的 Windows 构建 来源:https://algassert.com/post/2603 一切始于我向一位暑期实习生展示如何使用 `stimflow` 创建量子电路。我们注意到一个愚蠢的 bug:当 flow 有名称时,使用 `start="auto"` 添加 flow 会失败。修复很简单。我写好了修复,在 stim 的 GitHub 仓库上创建了一个 pull request……然后噩梦开始了。Windows 构建失败了。而且不只是这个 PR 的构建失败,而是*所有*PR 都失败。单元测试进行到一半时,持续集成会崩溃,并给出一个模糊但不祥的消息:"访问违规"。某些东西坏了,产生了一个可能导致安全漏洞的错误。 ## 依赖吐槽 现代软件工程令人沮丧的一点是,事物永远不会一直正常工作。你可以让某样东西*工作*,但没有什么能*持续工作*。最终某个地方的某个人会改变某些东西,然后你就会浪费一整天去搞清楚到底发生了什么。如果你有很多依赖,这种"无法持续工作"的问题会更糟。幸运的是,在编写 stim 时我采取了原则性的立场,使用了很少的依赖。 ……除了构建系统。如果你不知道的话:构建 Python 包是扯淡的。这个过程以极其脆弱著称,每年他们都会通过试图修复这个复杂问题而把事情搞得更复杂(https://xkcd.com/927/)。目前,构建包的推荐方法是使用 Docker 容器。否则,关于构建系统的太多细节可能会进入包中并导致问题。但这还不够;在完成容器化构建后,你仍然需要运行一个叫做 `auditwheel`(https://github.com/pypa/auditwheel)的工具来修复一些剩余的问题。 当然,构建 Python 包的扯淡是叠加在让东西跨平台构建的常见扯淡之上的。例如,你知道在 Windows 上找到 C++ 编译器有多难吗?他们发布了一个名为 `vswhere.exe`(https://github.com/microsoft/vswhere)的程序,专门用于解决这个任务。(你怎么找到 `vswhere.exe`?嗯,它应该在 `%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe`,当然。) 即使你用 `vswhere.exe` 找到了编译器,事情还没完。编译会失败,因为编译器抱怨找不到标准头文件,比如 `<windows.h>`。别担心;众所周知,在 Windows 上找到标准头文件也非常困难。还有*另一个*程序,`vcvarsall.bat`(https://learn.microsoft.com/en-us/cpp/build/building-on-the-command-line?view=msvc-170),专门用于解决这个任务。 ……也许你现在能理解为什么有人会摊手喊道:"好吧!我让别人去解决!",然后引入一个构建依赖。由于构建跨平台 Python 包非常复杂,我使用 `cibuildwheel`(https://github.com/pypa/cibuildwheel)来制作 stim 的包。`cibuildwheel` 解决了我眼前的问题,但它不是一个完美的库。这类库有一个讨厌的倾向:解决 90% 的问题,同时让剩下的 10% 难上十倍。简单的情况工作得非常好,但一旦你做一些非标准的事情(比如运行时检测 SIMD 支持),抽象就会弯曲。你最终不得不向工具 A 解释如何向工具 B 解释如何向工具 C 解释如何向工具 D 去请做这件简单的事,但不幸的是,这些解释的层次将那个简单的愿望变成了一些你写一次就再也不想想起的诡异咒语。解决这些弯曲的抽象不会带来持久的教训,也无法产生满足感;最多只会激起对抽象的潜在恐惧。 无论如何,所有这些是为了向你解释为什么"Windows 构建因访问违规而失败"是一个噩梦。它可能是我的代码中的 bug。可能是 `cibuildwheel` 中的 bug。可能是 GitHub Actions 中的 bug。可能是 Visual Studio 中的 bug。也可能是这些系统*交互*中的 bug。几乎没有线索可循,而且最后总以某种令人不满意的方式结束。雪上加霜的是,我无法在本地重现这个 bug。我没有 Windows 机器,而且 GitHub Actions 不支持本地执行。我唯一知道这个 bug 发生的环境是,在推送到 GitHub 触发的持续集成中的 Windows 构建。(我从未成功让 `cibuildwheel` 解释给 `setup.py` 解释给 `cl.exe` 去并行构建东西,所以这些构建需要几分钟。)进展将会很慢。 ## 焦土调试 当我在一个复杂的调试问题上卡住时,我会使用我所谓的核战略。我开始删除所有东西。找到不相关的东西,删除它,检查 bug 是否还在,重复。一直进行直到 bug 缩小到你能理解自己的愚蠢为止。(我有一段美好的回忆,向一位被某个 bug 卡住几天的同事演示这个方法。我同意帮忙。我说"你能删除那几行吗?",他们怀疑地回应"它们不相关,所以我们不需要删除",我重申"如果它们不相关,那我们*就可以*删除它们。删除!"。这样重复直到我们删除了"明显不相关"的、实际上却是问题根源的代码行。) Stim 有大约十万行 C++ 代码。为了只解决一个 bug 而删除这么多代码很麻烦。但 stim 是我的代码库,我非常清楚什么依赖什么,所以我有很多明显不相关的东西可以删除。我知道导致访问异常的具体调用是 `stim.Circuit.reference_sample`,它会触及代码库中相当大的一部分,但不是全部。而且我删除的东西越多,Windows 构建就越快。所以我卷起袖子,拖延了一周,然后开始行动。 我删除了生成图的代码。删除了推导检测器错误模型的代码。文档?删除了。除失败测试外的其他单元测试?删除了。解析和序列化?删除了。我砍砍杀杀,总体上玩得很开心。时不时来点创造性的破坏还是挺过瘾的。 除了删除,我还在简化。例如,被采样的电路包含指令 `MPP X0*X1 Y0*Y1 Z0*Z1`。这是一个相当复杂的指令,模拟器通过将其分解为更简单的指令来实现。为了能够删除与分解相关的代码,我用更简单的指令替换了 `MPP`。 **Bug 消失了。** 嫌疑锁定:分解方法。我恢复了 `MPP` 指令,bug 回来了。好的。 我添加了一些 printf 调试。 **Bug 消失了。** 等等,什么?操!这是一个海森堡 bug(https://en.wikipedia.org/wiki/Heisenbug)!海森堡 bug 很讨厌,但我知道我在哪里加了打印语句。我还不知道问题出在哪,但我知道这个混蛋住在哪里。它就在这里: ```cpp uint64_t CircuitInstruction::count_measurement_results() const { auto flags = GATE_DATA[gate_type].flags; if (!(flags & GATE_PRODUCES_RESULTS)) { return 0; } uint64_t n = (uint64_t)targets.size(); if (flags & GATE_TARGETS_PAIRS) { return n >> 1; } else if (flags & GATE_TARGETS_COMBINERS) { for (auto e : targets) { if (e.is_combiner()) { n -= 2; } } } return n; } ``` 算下来:大约 90% 的代码库被删除了,但这个函数仍然牢牢地嵌在剩下的 10% 中。我需要隔离它。否则你无法理解海森堡 bug。例如,这个 `CircuitInstruction::count_measurement_results` 是从 `Circuit::count_measurements` 中的一个循环调用的。如果 bug 实际上是在这里触发的,我不需要 `Circuit::count_measurements`。所以我修改了测试,直接调用 `CircuitInstruction::count_measurement_results`,绕过 `Circuit::count_measurements`。这保留了 bug,正如预期。 然后我尝试删除 `Circuit::count_measurements` 方法,它已经不再被失败的测试执行了。 **Bug 消失了。** 这……不祥…… 在将近三个小时里,我一直在切断 `Circuit` 和 `CircuitInstruction` 之间的联系。随机的事情会导致 bug 消失。我开始害怕 `Circuit::max_operation_property` 模板。因为 Windows 构建很慢,我经常一次测试 5 个以上的修改。我学会了同情 CPU 分支预测器。但我剪啊剪,当大切口失败时就用小切口。慢慢地,非常慢地,它分开了。最终,`Circuit::max_operation_property` 倒下了。然后是 `Circuit`,不久后是 `CircuitInstruction`。直到剩下的只有这个: ```cpp #include <cstdint> #include <iostream> uint64_t repro() { uint32_t targets[6]{0, 27, 0, 0, 27, 0}; uint64_t t = 6; for (size_t k = 0; k < 6; k++) { if (targets[k] == 27) { t -= 2; } } return t; } int main() { std::cerr << "t=" << repro() << "\n"; return 0; } ``` 他妈的编译器 bug。去 godbolt(https://godbolt.org/),粘贴上面的代码,将编译器设置为 `x64 msvc v19.51 VS18.6`,设置标志为 `/O2`,执行会打印 `t=8589934594`: > 禁用优化(标志 `/Od`),或者将编译器改为 `x86-64 gcc 15.1`,它会打印正确答案(`t=2`)。 快速浏览 godbolt 报告的汇编,bug 与自动向量化(https://en.wikipedia.org/wiki/Automatic_vectorization)有关。编译器试图将 32 位字打包到 AVX 寄存器中,然后以 8 个为一组进行 `== 27` 检查和条件 `-= 2` 累加。它做错了什么,最终导致一个不想要的 `0x2` 被存储到返回的 64 位字的高半部分(`8589934594` 是 `0x200000002`)。 我试图向微软报告这个 bug。根据搜索,正确的地方是他们的 Visual Studio 社区网站(https://developercommunity.visualstudio.com/VisualStudio/report?webReport=true)。唉,就像他们的编译器一样,他们的网站也是坏的。不登录就无法提交 bug,而它拒绝让我用 GitHub 账户登录,因为我关联了第二个 `@google.com` 邮箱地址(为什么?!): > 不,"下一步"按钮不起作用。在网站上创建新账户也不行,因为他们的"按住"验证码一直失败。所以我想编译器可以保持坏着,我就在这里抱怨好了。 ## 回顾与展望 回顾我用来焦土调试这个问题所花的 168 个(!)提交(https://github.com/quantumlib/Stim/pull/1077),我看到了各种我本可以做得更好的地方。 首先,我认为我焦土得有点太早了。单元测试只在单元测试中*第二次*调用引用采样方法时失败,而第一次调用没有使用 `MPP` 指令,所以我很早怀疑 `MPP` 可能牵涉其中。如果我更早地在那条执行路径上放置密集的 printf 调试,会省下一大段时间。 其次,我认为我应该更强烈地考虑使用 LLM 来完成这个任务。所遵循的迭代过程非常容易描述,但执行起来很繁琐。似乎非常适合 LLM。问题是 (a) 我不熟悉使用 LLM,以及 (b) 我他妈绝不可能让 AI 拥有对 stim 的无人监督推送权限。我可以通过练习使用 LLM 工具来解决 (a)。我可以通过从 GitHub Actions 切换到允许本地执行的持续集成系统来帮助 (b)。我还可以通过移除构建过程中的依赖来帮助 (b)。(有时候我梦想只是告诉 Windows 用户他们必须使用 Linux。他们不一定需要切换操作系统,但如果他们想使用 stim,他们至少必须在 WSL(https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux)中做。甚至有先例:这就是 tensorflow 所做的(https://www.tensorflow.org/install/pip#windows-native),至少在部分功能上如此。) 至于实际修复失败的构建,有人建议在相关的 `for` 循环上添加 `#pragma loop(no_vector)`。这个修复的问题在于,我不知道这是否是唯一受影响的循环。即使现在是,一个月后可能就不成立了。这次经历从根本上动摇了我对 Visual Studio 编译器的信任。所以我将剥夺它的优化权限。我让 `cibuildwheel` 解释给 `setup.py` 解释给 `cl.exe`,让它停止做编译器优化。这会使 stim 的 Windows 版本更差,但至少不会段错误。

相似文章

为 Windows XP 构建 Principia

Hacker News Top

一篇详细的技术博客文章,讲述了通过创建自定义交叉编译工具链,为 Windows XP 构建开源游戏 Principia 的过程。

我最喜欢的Bug:无效的代理对

Hacker News Top

一篇博客文章,回顾了一个Bug:在CRDT库中,插入相邻的多字节表情符号导致了一个拼接操作,分割了代理对,并静默地破坏了协作编辑器的同步。

后现代构建系统

Lobsters Hottest

一篇博客文章,探讨理想中的'后现代'构建系统的设计,该系统优先考虑可信的增量构建、最大化计算复用和分布式构建,并以Nix作为参考。