编程语言与软件工程2022arxiv.org
ISO C 已无法用于操作系统开发
ISO C became unusable for operating systems development
进阶 · 约 25 分钟读完前置知识:C 语言与指针、未定义行为(UB)、编译器优化
导读
这篇论文(作者 Yodaiken)论述 ISO C 标准中的未定义行为(UB)规则——即允许编译器假定某些情形永不发生——如何使 C 越来越难以可靠地编写操作系统这类底层代码,并主张应大幅削减或取消这类 UB。评论区的争论集中在:完全去掉 UB 是否会牺牲编译器优化能力,以及标准应当扮演「可移植汇编器」还是「高性能语言」的角色。
HN 关注点:HN 读者长期关心编译器利用 UB 做优化所带来的实际危害,以及 C 标准究竟该服务谁;这条帖子引发了关于优化收益与可预测性之间权衡的辩论。
HN 评论精选
这几条评论里:支持 1 质疑 1 补充 2
质疑UB 是基本优化所必需的,废除前需拿出性能代价的证据,但 C 的可靠性问题确实存在。
Ralf Jung 写了一篇博客回应本文的若干主张。他说论文很多观点都对,但作者因结论「应该完全废除这类未定义行为」而把婴儿和洗澡水一起倒掉。博客的目的是论证我们确实需要 UB:即使是所有编译器都会做的最基本优化,也需要这种影响深远的未定义行为概念。他坦言,基于另一种 UB 解释来写一个高度优化编译器是值得做的实验,因为我们极度缺乏「利用 UB 究竟带来多大性能收益」的数据;但他强烈怀疑结果能接近当今主流编译器,而能接受如此大性能损失的开发者大概本来就不会用 C。任何要求编译器收敛 UB 利用的提案,都必须给出证据证明这样做仍能让 C 胜任性能敏感场景。结论是他完全同意 Yodaiken 所说 C 有问题、自 UB 如此难避免以来可靠地写 C 已变得极其困难,减少 C 中可导致 UB 的构造、开发检测严格别名违规等高级 UB 的实用工具都值得做。
aw1621107查看英文原文
Ralf Jung has a blog post looking at some of the claims in this paper [0]. Some hopefully representative quotes: > The paper makes many good points, but I think the author is throwing out the baby with the bathwater by concluding that we should entirely get rid of this kind of Undefined Behavior. The point of this blog post is to argue that we do need UB by showing that even some of the most basic optimizations that all compilers perform require this far-reaching notion of Undefined Behavior. <snip> > I honestly think trying to write a highly optimizing compiler based on a different interpretation of UB would be a worthwhile experiment. We sorely lack data on how big the performance gain of exploiting UB actually is. However, I strongly doubt that the result would even come close to the most widely used compilers today—and programmers that can accept such a big performance hit would probably not use C to begin with. Certainly, any proposal for requiring compilers to curtail their exploitation of UB must come with evidence that this would even be possible while keeping C a viable language for performance-sensitive code. > To conclude, I fully agree with Yodaiken that C has a problem, and that reliably writing C has become incredibly hard since undefined behavior is so difficult to avoid. It is certainly worth reducing the amount of things that can cause UB in C, and developing practical tools to detect more advanced kinds of UB such as strict aliasing violations. <snip> > However, I do not think this problem can be solved with a platform-specific interpretation of UB. That would declare all but the most basic C compilers as non-compliant. We need to find some middle ground that actually permits compilers to meaningfully optimize the code, while also enabling programmers to actually write standards-compliant programs. [0]: https://www.ralfj.de/blog/2021/11/24/ub-necessary.html
补充语言应像 D 那样把字长、算术、浮点等固定下来,牺牲纸面可移植性换取实际可移植性与简单性。
多年前我在一次线上讨论中建议把 C 的 char 固定为 8 位。对方回应说存在 char 为 32 位的 CPU,而针对它的 C 编译器符合标准难道不好吗?我指出,几乎所有非平凡的 C 程序都得为此重写,那么标准允许这种做法究竟达到了什么目的?我也不认为该架构的编译器厂商做一个合理的 C 方言有什么问题——当年为迁就 x86 的内存架构,80 年代几乎所有 C 编译器都采用了 near/far 指针,虽不符合标准却无关紧要,而且非常成功。D 语言早期做的几个决定后来被证明很好:二进制补码回绕算术;基本整数类型大小固定为 char 1 字节、short 2、int 4、long 8;浮点为 IEEE;char 是 UTF-8 码元;char 无符号。这五点极大简化了 D 程序员的工作,讽刺的是还提高了 D 代码的可移植性。读完论文后,我倾向于修改 D 中 UB 的定义,使其不再意味着「可假定不会发生、非预期」。
WalterBright查看英文原文
I had an online discussion some years back where I suggested that C nail the size of char to 8 bits. He responded that there was a CPU that had chars be 32 bits, and wasn't that great that a C compiler for it would be Standard compliant? I replied by pointing out that nearly every non-trivial C program would have to be recoded to work on that architecture. So what purpose did the Standard allowing that actually achieve? I also see no problem for a vendor of a C compiler for that architecture making a reasonable dialect of C for it. After all, to accommodate the memory architecture of the x86, nearly all C compilers in the 80's adopted near/far pointers, and while not Standard compliant, it really didn't matter, and was tremendously successful. D made some decisions early on that worked out very well: 1. 2's complement wraparound arithmetic 2. sizes of basic integer types are fixed at 1 byte for chars, 2 for shorts, 4 for integers, 8 for longs. This worked out very well 3. floating point is IEEE 4. char's are UTF-8 code units (*) 5. chars are unsigned These 5 points make for tremendous simplicity gains for D programmers, and ironically increase portability of D code. After reading the paper, I'm inclined to change the definition of UB in D to not mean it can be assumed to not happen and not be unintended. (*) thanks for the correction
补充建议用「实现定义行为」或按架构制定 C 标准来替代大范围的 UB。
文章没提到的一点是:除了未定义行为,还有实现定义行为(implementation-defined behavior)。例如若把有符号整数溢出定为实现定义行为,那么任何异常都只限于那次溢出的整数运算本身。很多其他东西也可以表述为实现定义行为,这大概会砍掉一些优化。所以问题更像是:我们是否想要一个可移植的汇编器?若是,则尽可能多的 C 构造需要有定义的行为——或由标准定义,或写在编译器文档里。另一种可能是为 x86、amd64、arm 等分别制定 C 标准,这样就可以严格定义有符号溢出等行为;并规定在 x86 上指针没有对齐要求,指向合适大小存储的指针可用来存放另一类型的对象,等等。如果目标只是让 SPEC 跑得尽可能快,那么确保每个程序都触发未定义行为才是正道。
phicoh查看英文原文
One thing that is not mentioned in the article, is that next to undefined behavior, there is also implementation defined behavior. For example, if signed integer overflow would be implementation defined behavior, then any weirdness would be limited to just the integer operation that overflows. Lots of other stuff can be expressed as implementation defined behavior. That would probably kill some optimizations. So the question is more, do we want a portable assembler? In that case as many C constructs as possible need have defined behavior. Either defined by the standard or as part of the compiler documentation. Another possibily is to have standards for C on x86, amd64, arm, etc. Then we can strictly define signed integer overflow, etc. And say that on x86, pointers don't have alignment, so a pointer that points to storage of suitable size can be used to stored an object of different type, etc. If the goal is to run SPEC as fast as possible, then making sure every program trigger undefined behavior is the way to go.
支持以 Linux 早期坚持 GCC 2.95 为例,说明开发者宁要可预测的代码生成也不要激进的优化。
据我记忆,Torvalds 在 Linux 早期是 GCC 2.95 的坚定拥护者,因为他清楚它会生成什么样的代码,而不信任更新的编译器在那些情形下产出正确代码。今天要费尽周折去告诉编译器「不,你刚才说可能发生的事我确实就是想做」,这种代价恐怕难以承受。我觉得大概从自修改代码开始被嫌弃的时候,编译器就走偏了。
h2odragon查看英文原文
Torvalds was a strong advocate of GCC 2.95 (iirc), early on in Linux history, because he knew the kind of code it would emit and didn't trust the newer compilers to produce code that was correct in those circumstances. The workarounds and effort required to tell a compiler today that no, you really did want to do the thing you said might well be insupportable. I figure they started going astray about the time self modifying code became frowned upon.
相关资源
从这篇论文所有 HN 讨论帖的全部评论里整理出来的链接。
历次讨论
- 2022Hacker News 讨论帖203 分 · 475 评
同领域还值得读
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 分钟编辑精选