编程语言与软件工程2017microsoft.com
简单、快速且安全的手动内存管理
Simple, Fast and Safe Manual Memory Management
进阶 · 约 60 分钟读完前置知识:手动内存管理、垃圾回收基础、虚拟内存/MMU
导读
这篇论文提出一种在手动内存管理中加入运行时安全检查的方案:程序显式 delete 对象,但对象所在页会延迟回收;解引用已删除对象可能得到 DanglingReferenceException,调试模式可强制每次检查。为降低开销,它采用提升存活对象、惰性修补失效引用和编译器元数据等机制,目标是在接近手动管理的性能下提供内存/类型安全。评论指出其保证并非全覆盖,且实现可能引入类似 GC 的栈映射和 MMU/TLB 成本。
HN 关注点:集中在运行时异常式安全是否真的安全、是否退化成 GC,以及 MMU/TLB 与编译期保证的取舍。
HN 评论精选
这几条评论里:质疑 3 补充 1
质疑故障修复阶段需要栈映射区分指针与同地址值,实质上接近 GC,且难处理 NaN-boxing。
我可能理解有误,但一个反应是:手动内存管理的一大优势是不需要能识别所有存活引用的运行时——指针可以被转成 int、做 NaN-boxing 等。这个方案通过把存活对象复制到新分配区来“提升”它们,一直向前扫描避免 ABA,也不急切更新所有引用,而是允许引用悬空。于是,一个悬空的 NaN-boxed 指针之后被解包并解引用,会因页面不可读触发 SIGSEGV,信号处理器再尝试修复;问题就在这里:若对象已被提升,处理器要扫描所有寄存器和当前栈帧并修补陈旧引用,这要求运行时区分“指向已提升对象的引用”和“恰好共享地址的值”,几乎又需要栈映射。论文随后修改编译器,为每条 load/store 而非仅 GC 安全点生成堆引用位置元数据——这就等于在造垃圾回收器了,而且对 NaN-boxing 例子没帮助。
millstone查看英文原文
One reaction, with the caveat that I may have misunderstood some or all of this: One of the great advantages of manual memory management is that it does not require a runtime that can identify all live references. A pointer may be cast in an int, NaN-boxed, etc. - no problem. In this scheme the "promoting live objects" phase works by copying objects to a new allocation. Crucially, this avoids the ABA problem by always walking forwards in memory. Second, it does not attempt to eagerly update all references - instead it permits references to become dangling. So let's say we have a dangling NaN-boxed pointer. Now later, our NaN-boxed dangler is unpacked and dereferenced. This causes a SIGSEGV since the page is no longer readable. The signal handler notices it and attempts to fix it up. IMO this is where things go off the rails: > If the object was promoted, the handler scans all registers and the current stack frame, and patches all stale references to promoted objects. We were so close, but now the runtime needs to be able to distinguish references to promoted objects and values that happen to share their address. We almost got away without stack maps. > Therefore, we modify the compiler to emit meta-data describing the location of heap references in registers and on the stack for every load and store instruction (instead of just gc safe points) Ok I give up. Now we're just building a garbage collector. This does not help our NaN-boxing example. There's no way to easily inform the NaN-boxed value that it needs to update; we could hack it by comparing the register's value after the dereference, but that solution is hard to love. IMO more interesting would be to modify the compiler to require all dereferences to be updatable, and propagate that back through to the original reference. In C++ speak we could imagine this: void *dereference(void *&ptr) {...} and force the clients to deal with the fallout of back-propagating the new value. It's a lot of work, but the carrot is "pointers cannot dangle and you don't need a GC and you don't need Rust-style borrow checking" which sounds pretty rad. Anyways seems interesting but could be bolder?
质疑“成功或异常”的悬空引用语义在对象复用下仍可能泄露可利用数据,不能等同于完全安全。
论文用 DanglingReferenceException 保证安全,语义是:解引用已删除对象要么像对象尚未删除一样成功,要么抛出异常。我担心在可变大小对象复用/freelist 方案中,悬空引用即使不包含可控制程序流的垃圾,仍可能含有可被其他方式利用的数据,所以并非完全安全。调试模式下每次解引用已删除对象都抛异常的做法,也可用于测试 C++,例如用 Google Sanitizers;但整体仍是有趣且巧妙的结果。
7197ghr918hf查看英文原文
> We guarantee type and memory safety by introducing a new exception (DanglingReferenceException) with the following semantics: a dereference to a deleted object will either succeed as if that very object was not yet deleted, or result in a DanglingReferenceException. I would worry about what might happen in a variable-sized object reuse/freelist type scheme combined with this allocator. A dangling reference might not contain garbage that allows an attacker to control program flow, while still containing data that is exploitable in other ways. So this is not an entirely safe way to do things. Arguably it would be worth the speedup. But many of the techniques for making this approach safe . . . > such exceptions can be detected with the combination of rigorous testing and support in the allocator for a debug mode that enforces stronger semantics (i.e. exceptions on every dereference to a deleted object) at a higher cost . . . can also be used to test C++. For example, we use https://github.com/google/sanitizers a lot at the office to detect these sorts of errors. Still, an interesting and clever result. Nicely done!
补充方案依赖对象布局,void* 会破坏它,且未讨论进行中 I/O 缓冲区与惰性对象移动的冲突。
很巧妙。但它带有大多数需要知道对象布局的垃圾回收器的问题:void* 会破坏这一方案。论文没提 I/O:若有进行中的 I/O,其缓冲区需要锁定;对象移动是惰性的,所以还不算太糟,但要避免对象移动启动后与待处理 I/O 冲突而阻塞。不过这是微软在自己的运行时上做的工作,必要时可以改 I/O。
Animats查看英文原文
That's very clever. It has most of the problems of garbage collectors that need to know object layout. "(void *)" will break this. They don't mention I/O. If you have an I/O operation in progress, its buffer areas need to be locked. But the object-moving operation is lazy, so that's not too bad. You have to avoid getting into a situation where object-moving gets started, clashes with a pending I/O operation, and has to block. But this is Microsoft working on their own runtime, so they can fix I/O if needed.
质疑用 MMU 延迟回收和惰性修补引用,TLB miss 开销未获解答,且不防泄漏。
对已删除对象的访问是这样检测的:把对象标记为已删除,但不回收其内存,直到包含这些对象的页大部分都是已删除对象,再搬移仍存活的对象并 unmap 页面;也就是用 MMU 技巧并摊薄其成本,否则性能会很糟。它仍需重写对象引用,很像 GC——不过它是惰性修补引用,仍有开销。它也不能防内存泄漏。想法有趣但太晚,我认为目前 Rust 是更好的答案。视频问答里有人问“为什么你们没被 TLB miss 拖垮”,讲者不知道,这是个很重要的问题。
cryptonector查看英文原文
Access to deleted objects is detected by marking objects deleted and not reclaiming their memory until the containing pages are mostly full of deleted objects, then they relocate still-live objects and unmap the pages. That is, they use MMU tricks but they amortize the MMU cost (otherwise performance would be absolutely abysmal). Note that they still need to rewrite object references, which is a lot like a GC. EDIT: No, sorry, they lazy patch references, though this requires some overhead. Also, this does nothing to protect against leaks. Interesting idea, but too-little-too-late. I think Rust is the better answer for now. EDIT: Great question from the Q&A of the video, "why are you not getting killed by TLB misses?", and the presenter did not know. This is a really important question.
相关资源
从这篇论文所有 HN 讨论帖的全部评论里整理出来的链接。
历次讨论
- 2017Hacker News 讨论帖115 分 · 17 评
- 2019Hacker News 讨论帖60 分 · 12 评
同领域还值得读
Google 的软件工程(2017)
Software Engineering at Google (2017)
介绍了 Google 的软件工程实践,包括其单一代码库(monorepo)、几乎全部开发在主干进行而非分支、代码审查、构建系统 Blaze、协议缓冲区等工具与流程,并讨论了频繁重…
编程语言与软件工程2017 · 1,127 分 · 465 评 · 引用 15入门 · 约 45 分钟编辑精选Peter Naur:编程作为理论构建 (1985)
Peter Naur – Programming as Theory Building (1985)
Peter Naur 1985 年的经典论文《Programming as Theory Building》提出:编程的本质不是写代码或文档,而是在程序员头脑中构建对程序及其运行…
编程语言与软件工程2016 · 629 分 · 74 评入门 · 约 45 分钟编辑精选SciPy 1.0:Python 科学计算的基础算法
SciPy 1.0: fundamental algorithms for scientific computing in Python
这篇发表于 Nature Methods 的论文介绍了 SciPy 1.0——Python 科学计算生态的基石库,涵盖其核心算法模块(优化、积分、插值、线性代数等)、软件架构与社…
编程语言与软件工程2020 · 387 分 · 98 评入门 · 约 45 分钟编辑精选Lisp 的历史(1979)
History of Lisp (1979)
这是 Lisp 发明者 John McCarthy 于 1979 年为 HOPL(编程语言史)会议撰写的回顾文章,讲述 Lisp 的起源、设计动机与早期演进,文中也涉及 if 表…
编程语言与软件工程2018 · 352 分 · 44 评入门 · 约 45 分钟编辑精选使用 NumPy 进行数组编程
Array Programming with NumPy
这篇 Nature 文章介绍 NumPy 的数组编程模型及其在科学计算中的核心作用:以 ndarray 为中心,通过向量化操作让 Python 高效处理多维数组。
编程语言与软件工程2020 · 318 分 · 134 评入门 · 约 40 分钟编辑精选