让每个 C 程序都证明自己没有内存泄漏
一篇带有玩笑性质的博客文章提出通过拦截 `malloc` 并把每次分配都存进全局列表来“修复” C 的内存泄漏,让泄漏检测器看到的总是“仍然可达”——代价却是真正制造泄漏并破坏内存。评论者分析了这种做法为何不安全且具有误导性,将其与 Valgrind 和 LeakSanitizer 等真实工具对比,并借此讨论何时可以不释放内存(短生命周期进程、arena、FaaS),以及何时必须依赖健壮的内存管理、RAII 或垃圾回收。
提案的严肃性
- 许多评论者认为这篇帖子明显是在开玩笑,目的是展示泄漏检测器有多容易被糊弄,而不是作为生产环境技术。
- 也有人指出,这段代码看起来足够像那么回事,以至于经验不足的 C 程序员可能会把它当成真正的解决方案,这让他们很担心。
对 “bigbucket” malloc 包装器的技术批评
- 这个包装器只是在一个全局列表里跟踪分配;即使用户代码调用了
free,它也从不删除条目。 - 这会产生真正的泄漏:用于跟踪列表的堆内存会无限增长,而存储的指针在释放后会变成悬空指针。
- 即使程序在循环中正确释放了一切,它也可能耗尽内存。
- 它不是线程安全的,并且每次分配都调用
dlsym,代价很高。 - 有人指出,你可以通过把
free重新定义为无操作来把这个玩笑“补完”,这样就能彻底避免悬空指针。
泄漏与无界内存增长的定义
- 一些人指出,“可达”的内存在实践中也可能算泄漏;带 GC 的语言也可能因为忘记的引用而泄漏。
- 另一些人强调,真正重要的是无界的内存消耗,而不是内存是否在技术上仍然可达。
真实系统中的故意不释放
- 许多例子表明,不释放是有意且可接受的:短生命周期程序、按请求处理的 CGI/PHP 风格进程、arena/按请求分配器、高频交易系统、一些游戏/音频引擎、导弹或专用嵌入式系统,以及会定期重启的服务器。
- 有人认为,在程序退出时释放通常没什么意义,甚至可能损害性能(例如在拆除大型堆时引发页面错误)。
泄漏检测工具与实用策略
- 评论者推荐使用 Valgrind 和 LeakSanitizer 之类的真实工具,以及 arena、按模块无泄漏设计、以及选择性释放占据大多数分配量的“热路径”等模式。
- 还提到一种技巧:只在检测到泄漏检查器运行时才执行完整清理逻辑(例如在运行时检测 Valgrind)。
元问题与度量问题
- 讨论串反复提到,如果你只优化“在工具 X 下没有泄漏”,人们就会想办法钻这个指标的空子(古德哈特定律)。
- 还有人指出,在大型代码库中,真正的泄漏往往比因保留但未使用的数据造成的内存膨胀更少见;后者更难发现,而且会影响所有语言。