HackerTube English?
软件边界与变化 →

接口藏起来的成本,最后由谁承担?

零开销抽象、换一家内存分配器、在内部用并发:接口让调用者不必关心实现,但时间、内存和协调的成本并没有消失,只是换了承担者。

本文的问题 · 10
  1. 接口藏起的成本去了哪里?
  2. 串行程序可以有多少种执行顺序?
  3. “这个任务不能读它”如何被发现?
  4. 框架的承诺,一定要由浏览器里的运行库兑现吗?
  5. 内部按并发来想,外部一定要暴露并发吗?
  6. 一次编译没有额外开销,承诺由谁负责?
  7. 接口的简单,要由谁来维持?
  8. 换一家分配器拿到的内存,释放时怎么还回去?
  9. Lisp 能生成机器码,就能直接调用现成的 C++ 函数吗?
  10. 泛型代码少写一份,专门化的成本由谁承担?

接口藏起的成本去了哪里?

同样一次哈希查找,可以顺着密集的数据往后探测,也可以追着指针去很远的地址。Carruth 的数据结构演讲让你沿实际访问路径计成本。把同样的问题带到他的抽象开销演讲:即使一个调用点产生了漂亮指令,成本也可能留在编译时间,或出现在跨函数的调用约定上。对调用者说「快」以前,先说明承诺的是哪种负载、哪道边界,以及谁在付费。

原文与时间戳 · 2
maximize the cache locality of of handling a collision
CppCon 2014: Chandler Carruth "Efficiency with Algorithms, Performance with Data Structures" · ▶ 49:33–49:43
it just means the cost is paid somewhere else
CppCon 2019: Chandler Carruth “There Are No Zero-cost Abstractions” · ▶ 16:44–16:51

串行程序可以有多少种执行顺序?

把一个乘积展开,先乘左边还是右边,草稿不同,答案仍相同。Wolfram 借这个例子说明为什么要单独检查重写规则是否允许交换次序。Keller 面对的是一条真实的指令流:处理器找依赖、提前执行独立指令,还要记住哪些结果能按要求完成。两者之间的联系在于先声明外部允许观察什么。代数例子只比较最终表达式,处理器还需遵守内存等可观察行为,单看最后一个数可能漏掉违约。

原文与时间戳 · 4
you'll get different orders
Stephen Wolfram: Fundamental Theory of Physics, Life, and the Universe | Lex Fridman Podcast #124 · ▶ 95:37–95:40
you'll always get the same answer
Stephen Wolfram: Fundamental Theory of Physics, Life, and the Universe | Lex Fridman Podcast #124 · ▶ 95:29–95:31
go figure out the dependency graph
Jim Keller: Moore's Law, Microprocessors, and First Principles | Lex Fridman Podcast #70 · ▶ 8:24–8:29
what order should these operations finish in
Jim Keller: Moore's Law, Microprocessors, and First Principles | Lex Fridman Podcast #70 · ▶ 8:01–8:05

“这个任务不能读它”如何被发现?

两个工作线程这一次碰巧按理想顺序结束,屏幕上的一帧也可能完全正确。《命运》把另一种观察安插在开发版本中,检查任务依赖图,并在访问数据时核对权限。读者可据此改写一个测试问题。除了“最后一帧画对了没有”,再问“产生这一帧时,有没有人读取自己未声明可读的数据”。第二个问题才会在好运气的调度里留下证据。

原文与时间戳 · 2
identifying unsafe patterns, but it's going to be doing it during development
Multithreading the Entire Destiny Engine · ▶ 21:47–21:52
every time that you access the data, it's checking dynamically right there and then for whether or not you have permission
Multithreading the Entire Destiny Engine · ▶ 52:31–52:37

框架的承诺,一定要由浏览器里的运行库兑现吗?

Harris 为模块边界补充了一个时间维度:开发者使用的声明式组件接口,不必原样成为用户浏览器里运行的机制。他在介绍 Svelte 3 时提出,把一部分组织工作放到构建阶段,让生成的代码完成页面更新;对过渡、绑定等可选功能,他的目标是只为实际使用的部分生成支持代码。这里要区分「工具提供哪些能力」和「某个应用实际交付哪些代码」。检查自己的抽象时,可以先列出调用方需要表达的意图,再问哪些决定在构建时已经可知、哪些必须等到运行时。这个例子扩展了隐藏实现决定的思路,不能仅凭演讲就保证任意程序都更小、更快;本文也没有复核屏幕上的代码或基准结果。

原文与时间戳 · 2
a framework doesn't need to be a thing that runs in the browser
Rich Harris - Rethinking reactivity · ▶ 6:53–7:02
we don't need to include the code that makes that feature possible
Rich Harris - Rethinking reactivity · ▶ 33:35–33:42

内部按并发来想,外部一定要暴露并发吗?

Pike 先让扫描器和解析器各自保留控制流,再讨论怎样让调用方只看到「取下一个词法项」的接口。对模块承诺来说,值得分开记录的是:调用者怎样获得下一项,内部怎样保存扫描到哪一步,以及是否真的同时运行多个工作单元。他强调并发能够支持并行,却不要求并行;不能把设计中的独立活动直接改写成吞吐保证。问答里他承认没有测量性能,也愿意在性能关键时改用别的缓冲实现。因此可以把这个例子当作接口与内部组织可以分离的设计讨论,而不是某种通道实现总是更快的证据;本文未复核屏幕代码,也不沿用当时的初始化限制解释今天的 Go。

原文与时间戳 · 2
concurrency enables parallelism doesn't require it
Lexical Scanning in Go - Rob Pike · ▶ 39:20–39:26
to be honest I have not measured it
Lexical Scanning in Go - Rob Pike · ▶ 43:09–43:13

一次编译没有额外开销,承诺由谁负责?

Turner 的演讲为模块承诺添了一道验证边界。他描述一次加入 const 后的优化时,并没有把它说成所有编译器都保证如此,而是说明当时只有自己试到的一种编译器与标准库组合实现了结果。若调用者既要可读的接口,又依赖某种开销上限,就要把功能约定和实际构建中的成本证据分开。可以为项目写下工具链版本、优化选项、目标机器,以及变更后要重新检查的输出。这个检查思路来自讲者的限定,不表示本站已经重现了演示,也不说明对象、lambda 或生命周期封装在所有场合都没有开销。

原文与时间戳 · 2
the compiler happened to be able to optimize it.
CppCon 2016: Jason Turner “Rich Code for Tiny Computers: A Simple Commodore 64 Game in C++17” · ▶ 28:45–28:56
that was the only combination I found
CppCon 2016: Jason Turner “Rich Code for Tiny Computers: A Simple Commodore 64 Game in C++17” · ▶ 28:58–29:06

接口的简单,要由谁来维持?

Rob Pike 用 Go 的导入语句解释,调用方看到的一行代码背后仍有命名、作用域和链接要处理。Eskil Steenberg 从另一头描述同一项工作:先写出调用方需要的接口,再填入实现。这两种做法都把读者要理解的细节留给了库或语言的维护者。

Linus Torvalds 的问答给这个选择加上了时间尺度。内核维护者把已有用户空间程序能继续运行当作约束,演讲中也承认会出现破坏兼容性的情况。设计一个容易使用的入口,与日后让已有调用者继续使用它,是接口生命周期里的两项工作。

原文与时间戳 · 4
it interacts with things like scoping, naming, information hiding, isolation, linking,
dotGo 2015 - Rob Pike - Simplicity is Complicated · ▶ 19:59–20:09
I start from the outside usually
How I program C · ▶ 35:28–35:33
we don't break user space
DebConf 14: QA with Linus Torvalds · ▶ 8:39–8:47
we do break user space sometimes
DebConf 14: QA with Linus Torvalds · ▶ 46:49–46:56

换一家分配器拿到的内存,释放时怎么还回去?

Alexandrescu 用内存分配器说明,能否组合接口要同时看取出和归还两个方向。他在演讲的设计里用指针和长度一起表示内存块。主分配器满足不了请求时,可以让后备分配器再试一次。到释放时,同一个入口仍要知道该把块送给谁。他用主分配器的归属检查说明这种路由怎样实现。检查一个组合接口时,可以沿着同一块内存走完两程,先问谁有能力交付,再问哪项约定能把它送回原处。

原文与时间戳 · 2
it has the pointer and the length
CppCon 2015: Andrei Alexandrescu “std::allocator...” · ▶ 8:11–8:18
if you want to write an allocator you got to make it composable
CppCon 2015: Andrei Alexandrescu “std::allocator...” · ▶ 23:34–23:45

Lisp 能生成机器码,就能直接调用现成的 C++ 函数吗?

Schafmeister 介绍 Clasp 时,分别交代了编译 Lisp 和连接 C++ 库的办法。Clasp 的编译器生成 LLVM 中间表示,再由 LLVM 生成机器码。要从 Lisp 调用演讲示例里的 C++ 函数,他还在绑定库里提供 Lisp 名称和 C++ 函数指针,让库在编译时生成包装器。追踪这次调用,可以在两处检查交接。编译器把什么交给后端?包装器又怎样把 Lisp 端可见的名称接到已有函数上?

原文与时间戳 · 2
implemented a new Common Lisp that uses LLVM as the backend.
Clasp: Common Lisp using LLVM and C++ for Molecular Metaprogramming · ▶ 20:24–20:31
and it builds the wrapper function at compile time.
Clasp: Common Lisp using LLVM and C++ for Molecular Metaprogramming · ▶ 39:58–40:01

泛型代码少写一份,专门化的成本由谁承担?

Smith 用 Coalton 说明,泛型接口可以把选择具体运算的工作交给编译器。编译器看到类型后,为相应组合生成专门代码。这条路径需要能取得源码或相应表示,也可能生成更多代码。理解演讲里的性能案例时,要把运行时间、编译工作与代码体积一起纳入比较。

原文与时间戳 · 2
much more than safety.
Toward safe, flexible, and efficient software in Common Lisp - European Lisp Symposium 2025 · ▶ 14:51–14:59
you can't monomorphize if you don't have the source code.
Toward safe, flexible, and efficient software in Common Lisp - European Lisp Symposium 2025 · ▶ 43:30–43:35

读完再想一步

挑一个你常用的抽象:它省下了什么,又把哪项成本推给了编译器、运行库或别的团队?