为什么我不喜欢用建筑比喻软件

2011/09/24

最近在读《 Code Complete, 2nd Edition 》。谈论比喻 (metaphor) 的重要性时,这本书把建筑作为软件开发的主要比喻之一,并且认为这个比喻很贴切很有用。我认为这本书有很多正确的结论,但不包括对这个比喻的看法以及随之而来的某些直接推论。

对于一个建筑,很容易区分哪些是基础,哪些是附属(或者分辨各个部分更接近基础还是更接近附属的程度)。墙面的装潢很容易修改,改变墙体的构造就很困难。对摩天大楼来说,改变顶层的尖塔很容易,而改变首层的承重结构就几乎不可能。基础和附属在修改成本方面的巨大区别令人直观的意识到规划的重要性。构建一个各部分都「摸得着、掰得下来」的狗窝和构建一个很多东西「封在里面、压在底下」的摩天大厦绝对需要完全不同的规划方式、设计手段、和管理模式。对于后者,构建之前的确需要仔细分析,针对基础部分的决定一旦作出很难更改。

但是把这个比喻延伸到软件会引入众多似是而非的问题。可以从修改的难度来界定软件的基础和附属吗?可以从暴露的程度来区分软件的核心和外围吗?让我们尝试几种判断的方法。

首先,可以把软件系统中靠近硬件的部分,或者被最多的模块依赖的部分称为基础吗?可是,有无数的成功实例说明应用软件可以从一个操作系统移植到另一个操作系统,或者一个操作系统从一种内核迁移到另一种内核。在整个软件栈 (software stack) 中这种釜底抽薪的替换工作并不鲜见。在软件栈稍靠上的地方,也不乏保留上层模块而替换相对底层模块的例子。就在最近的 Adobe Creative Suite 5 for Mac 大部分产品从 CS4 的基于 Carbon 转换到基于 Cocoa。

可以把靠近用户的部分称为基础吗?大多数完全不了解编程的人可能都会反对。软件的界面、皮肤、look and feel 都是可以不断变化的。可以把与具体算法无关的机制称为基础吗?不久前我的 blog 里关于微内核和 XNU 的文章里可以看到内核的演化中,各个模块的运行级别可以在内核态和用户态之间切换,通信机制也可以发生变化。可以把与具体机制无关的算法称为基础吗?Linux kernel 的调度算法在不改变进程切换机制的情况下从线性时间复杂度变成常数时间复杂度。Mac OS X 的 built-in VNC 实现 (Share Screen.app) 在不修改基本机制的情况下加入了大大优于其它实现的屏幕数据压缩算法。符合标准的接口和协议是不能轻易修改的基础吗,比如 x86 指令集或者明文协议?你总是可以用某种中间翻译层来替换它们,比如 VMWare、Linux 的 Wine、Windows 下的 Cygwin、以及 Mac 下的 Rosetta 或者 ssh tunnel 这样的 gateway 方案。

所以,软件和建筑根本的不同是前者不可能存在被「封在里面、压在下面」的「基础」。《 The Art of UNIX Programming 》里告诫程序员,让软件系统长久保持生命力的唯一手段,是把它分解成功能独立而且可以替换的模块。换句话说,软件系统中出现了如同建筑中那种不能轻易修改的基础才是失败的设计。软件需要一定程度的规划,却并非如同建筑所比喻的那样。相反,这种规划是为了让软件具有完全不同于建筑的灵活性。软件同样需要事前考虑避免重复投入,但是在软件系统中,并没有建筑中那种一损俱损的中心基础模块,修复不同模块的设计缺陷的花费并无数量级的区别。适度的事前规划软件开发是必要的,但是我见过很多的新手惧怕的风险其实来自「建筑的基础」这个蹩脚的比喻,事后往往证明,很多在他们看来很严重的必须在 coding 之前分析透彻的潜在问题,都能非常容易在 coding 进行到中后期的时候被解决。而且在这个时候,他们往往已经从 coding 和初步的 testing 中吸取了很多事前分析无法获得的知识和分析手段。而他们认为的一些「关键」组件,即使不幸存在设计缺陷,也并不比外围组件的缺陷修复起来更难。

MVC:用来打破的原则

2011/09/07

去年的《 C 是 MVC 的 C 》简单写了些 MVC 的基本原则,特别是关于 controller 这个经常被忽视和误解的模块。有人认为对 controller 的不同认识叫做 MVC 的不同「流派」或者「变种」,我不能认同。我相信只有符合《 C 是 MVC 的 C 》里所述原则的 MVC 是降低整体复杂度的最佳方案。另外,我最近在读《 The No-Designer’s Design Book 》,并且向没机会系统学习设计的 UI 开发者推荐这本书。为了在一本书的篇幅里让 no-designers 把握设计的关键,它没有讨论什么不同的「流派」,而是把产生好设计的要素归结为四大通用原则。

有人可能会对上面两件事产生同样的质疑 —— 面对一个博大的领域,拘泥于几条有限的原则,是不是纸上谈兵,削足适履?我这篇 blog 就是想写些实际开发中遭遇的细节与原则之间的冲突。对于这种冲突,《 The No-Designer’s Design Book 》有两句精彩的总结:

  • You must know what the rule is before you can break it.
  • Either do it all the way or don’t do it. Don’t be a wimp.

UI 程序的设计开发就是关于遵循和打破 MVC 原则的权衡,而这个权衡恰恰说明了坚持唯一原则的重要性。我相信在唯一原则下有意识地考虑取舍优于在几个所谓 「流派」之间选择或者摇摆。

反面向对象的 Model

设计模式产生于面向对象的方法。但 MVC 最有意思的一点是 model 的设计原则体现出反面向对象思想。面向对象的第一要旨是数据和行为的统一。Model 表示一组数据,对于一组数据来说,最重要的行为有两类,一是在某种场景中体现自己,二是对外界的交互产生反应改变自己。MVC 模式里,第一类行为被划分为 view 的职责,第二类行为被归为 controller 的职责。Model 呢?Model 就是一组数据而已。Model 模块被剥夺了「对象」的特质。MVC 的理想状态是回归到「数据」加「算法」的传统模型。从另一个角度看,我经常说面向对象技术不适合 UI 之外的领域。作为 MVC 中和 UI 本身关系最远的模块,model 印证了这个观点。

主要行为被 view 和 controller 剥夺之后,model 勉强保留了一个行为 —— 保持数据的一致性。在《无毒副作用》中提到过,保持一致性并非本质上不可或缺的能力,没有冗余的理想 model 无需任何行为就能天然地保持数据一致性。所谓冗余就是指 model 中的一部分数据总是可以被另一部分数据计算出来。那些不能从其它数据计算出来的最基本的数据即「自由变量」,被计算出的数据即「依赖变量」。保持一致性的行为就是用自由变量重新计算和更新依赖变量的操作。在理想条件下,「依赖变量」应该作为 view 重新绘制时的临时数据而非 model 的一部分。

但现实中 model 终归无法避免冗余,所以本质上不必要的行为在实际中是不可或缺的。保持数据一致性的行为是 model 设计中细节复杂度的主要来源。比如在复杂耗时的数据变换中,如何在达到最终数据一致的过程中,找到保持局部一致性的中间状态,并且通知 view 进行平滑的显示。这在《并发与并行》中有所讨论。Model 是理想中的傀儡,在向现实的妥协中成长为一个负责的对象。

性能与一致性

在计算机领域,最伟大最纯粹的设想往往只为一个原因妥协 —— 性能。MVC 也不例外,它的理想是通过牺牲性能来降低整体复杂度:无冗余的 model 从根本上杜绝数据不一致的问题。一旦需要,view 就遍历整个 model,计算所有绘制所需的「依赖变量」,最后重新绘制界面。每个 UI 的实现,为了避免 premature optimization,都应该从这个状态开始。但是当 UI 真正发布的时候不可能还保持这种干净的状态。UI 迭代开发过程的重要部分就是不断根据需要在 model 中添加「依赖变量」来满足 view 的性能需求。引入「依赖变量」会增加 model 数据不一致的机会。如果为了抵消这种风险而过多添加和调用保持数据一致的代码,反而会导致性能损失。开发 UI 的整个过程是仔细权衡数据冗余和一致性级别的调优过程。也许构建强大的服务后端难度不亚于建设一个电站,但构建精致的 UI 也不亚于设计一台精巧的引擎。

如上所述,model 的设计要考虑几种平衡:

  1. 如何以及在何种程度上引入「依赖变量」。
  2. 在何时进行保持数据一致性的工作。其可能的时机包括 controller 的事件处理,view 的更新,后台 thread 或者 timer。

这些决定会影响 controller 和 view 的处理时间,而 controller 和 view 的处理方式恰恰是不同平台处理方式差异最大的地方:

  1. 平台会以什么样的频度对 controller 和 view 分别发出事件?比如,在不同系统中,mouse move 的频度有很大区别。
  2. Model/controller 通知 view 更新的机制会被如何处理?在有些平台上,这种通知是不可靠的, 在频率过高的时候会被 view 忽略,而在另一些平台上,每次通知都会得到 view 的完全处理。

所以,UI 的用户体验问题经常会从这种平台差异中显现。虽然没有统一的方式来解决它们,但是有一个很好用的简单方法来判断问题的类属:如果在  controller 或 view 的处理函数中稍稍改变处理频度(比如三次调用中的两次处理成 no-op 直接返回),问题的症状会发生变化甚至消失,那么基本上就是数据冗余和保持一致性的级别问题。对于如何设计「依赖变量」,不光要考虑冗余数据量的多少,也要考虑冗余的层次,即某些「依赖变量」是由其它依赖变量计算而来,为高一级的依赖变量。在需要快速响应的情况下,不更新或者只更新少量低一级的依赖变量,同时尽量使用之前计算好的依赖变量。在可以放慢响应速度的时候再更新各级依赖变量。

另一方面,某些「自由变量」在加入时间约束之后,可以简化为更少量更精简的数据,也就是在 model 暂时存入「变化量」,让 model 暂时维护 change 而非纯粹的 state,可以进一步减少 controller 的处理时间。从这个意义上说,自由变量是时间和变化量的「依赖变量」,在 model 中暂存变化量是对自由变量的「延迟存储」,是另一个方向上的冗余。

背离理想状态是为了更好的处理某些操作,但背离的程度越高,MVC 系统的弹性就越差。MVC 的弹性表现在可以处理设计者预料之外的用户操作。理想状态的 MVC 可能会因为性能低下而表现出响应不够敏捷,或者显示的更新不够平滑,但是不会陷入不一致的状态,甚至是除非关闭程序否则无法恢复的状态。而增加数据冗余的系统面对用户的随意操作其崩溃的危险也会增加。关于理想 MVC 系统弹性的讨论详见《 C 是 MVC 的 C 》。

Model 的 View 还是 Viewer 的 View

View 是 model 的 view 吗?回想一下别人或者你自己解释为什么系统中要有多个 view 的时候是否举过这个例子:有一组数据,又要显示成直方图,又要显示成饼图,又要做成表格 ⋯⋯  这个例子里,view 是 viewer 的 view 而不是 model 的,因为 view 的类型是为了满足 viewer 的解读而不是 model 的结构。但是,作为 viewer 的 view 面临一个难题,viewer 需要的并非总是直方、饼、表那样泾渭分明的形式,他可能需要一个表格里有饼图,另一个饼图里有表格。另一方面,model 里的某些数据可能在多个 view 里都要显示成同一种形式。

如果把 view 按照 viewer 的解读形式来组织,那么在处理混合显示时就可能不得不在多个 view 模块里加入重复的代码。有人说给不同 view 加上 common base class,建立 view 的继承树。我不相信这种方法指导下产生的代码是可持续维护的。另一个诱惑是把这些代码移到 model 中,让数据自己显示自己。但 model 夺取本该属于 view 的行为是对「关注隔离 (separation of concern) 」的违背。而且,MVC 系统只有一个 model,让它背负 view 的责任容易让 model 需要实现的操作数量面临组合爆炸 (combination explorsion) 的困境。

所以,除了 viewer 的 view,在复杂系统里还必须实现 model 的 view。为某些数据编写负责显示它们的 view 模块,让 viewer 的 view 来重用。在有些 UI framework 里有这种 model 的 view,比如 Java Swing 中的 renderer。这些模块始终都应该是 view 的一部分,而非 model 的行为。除了「维护数据一致性」,不要随意给 model 增加任何行为,因为「维护数据一致性」本身已经是背离理想状态的产物。

最成功的对象

如果说面向对象在 UI 之外的领域是失败的,那么反过来考虑,如果 view 都不能作为「名副其实」的对象那么面向对象这门技术也就一无是处了。而 view 确实在某种意义上是最「成功」的对象,它的行为最复杂,同时也拥有数据。

View 的行为复杂不必多说,它的绘制代码是用户直接接触的整个应用最炫的部分。但 view 不适合直接拥有数据,因为那是 model 的责任。那么 view 如何拥有数据?答案是:MVC 是可以嵌套的,而这种嵌套就直接作用在 view 上。在嵌套形式中,内层 MVC 作为外层 MVC 的 view 的一部分,外层 view 操作或者直接作为内存 MVC 的 controller ,外层 view 的一部分或者全部绘制行为是从外层 model 向内层 model 搬运(经过转换的)数据。经典设计模式中的「组合模式 (composition patter) 」的一些实际应用就是多层嵌套 MVC。常用 UI framework 提供的现成 control 通常就是局部的微型 MVC 系统。嵌套 MVC 是 view 的主要数据构成,也是 model 数据冗余的另一种形式,维护一致性的时机一般在最外层 view 的绘制操作中。

所以,当系统的某一部分过于复杂但又有很强的内在联系,不必强求把数据和行为分拆到系统的最外层 MVC,而是可以把复杂的部分实现成内层 MVC。代价是内外层的 model 之间会有一定程度的数据冗余。

MVC 模式的三个模块,controller 趋于分散(见《 C 是 MVC 的 C 》),model 趋于纯数据结构,view 强于行为,有时也间接拥有数据。MVC 和面向对象是共同产生的,它们的外延也趋于一致。MVC 看似描述了三个对象,其实是一个对象拆开的三个部分。

为什么 Mac OS X 先进?

2011/08/19

这个世界上,接触过三大主流桌面操作系统的人,总会有相当一部分承认 OS X 的相对先进,也会有很多人反对。我认为讨论 OS X 先进性的文章里《开发人员为何应该使用 Mac OS X 兼 OS X 小史》是比较全面的。包括 OS X 先进的图形系统,完全继承 UNIX 的命令行优势,发扬光大的 scriptability ,以及让开发者有机会避开在其它平台上避之不及的 C++ 等等。还有 2003 年出版的《 The Art of UNIX Programming 》,虽然当时 OS X 尚不成熟,作者 Eric Raymond 已经对其继承 UNIX 的风格大加赞扬了。

如果对这些赞扬 OS X 先进性的文字不屑一顾,也请忽略我这篇「枪文」,因为我想的问题更虚无:不是要在上面的鸿篇巨著后面再添上 OS X 如何先进的证据,而是思考一下促成这些成就的原因。

我认为根本原因在于 OS X 是一个「新」的系统。它的开发始于 1997 年。那时,操作系统中各种技术决策的优点和失算之处都逐渐显露出来。拿窗口管理系统 (window manager) 来说,UNIX 的 X 的体现了分离窗口系统和系统底层内核的正确性,也体现了把上层策略(如窗口的 theme/style )和底层机制分离的正确性。但是 X 系统也有层次过多和协议过于繁琐的缺陷,让 UNIX/Linux 在图形硬件高速发展的时期陷入困境。OS X 作为后来者有机会审视和改进这一切。所以它最终采用了和 X 相似的用户态窗口管理系统,采用了先进的图层混合 (layer blending) 模式,同时抛弃了繁琐的 X 协议和 client-server 架构。另一个例子是绘图模式 (drawing model) 。Windows 的 GDI 绘图模式在当时质量和能力已显粗糙,而在出版业 PDF 正在成为标准,所以 OS X 采用了 PDF 成熟的绘图模式。几年以后 Windows 才推出 GDI+ 效仿 PDF 的高级绘图模式,但是让 Windows 开发者完全脱离旧的 GDI 模式还要等上几年。还可以举出的例子是,究竟让 toolkit(按钮等控件)作为用户进程的一部分来管理,让窗口系统只负责像素;还是像 Windows 那样把 toolkit 作为内核对象来管理。Cocoa 给出的答案也是对先行方案的反思和改进。同样,几年以后,.NET 的 WPF 才开始仿效这种让窗口管理和 toolkit 分离的做法。

所以,公平地说,Windows 的很多问题在于它的「陈旧」,甚至可以归功于它是「先行者」。我想有些读者可能已经等不及要反驳我了:第一,上面说的很多技术(比如 X )并不是在 1997 年前后才显露其优势和不足的,它们的成熟和优劣定论要早得多;第二,OS X 的开发不是 1997 年才开始白纸一张地开始,它的前身 NeXTSTEP  在1980 年代中期就动土了。

没错,反驳的一语中的!即便如此,我仍然认为 OS X (NeXTSTEP) 有机会审视和反思前人的优秀技术(或者优秀的失误),而 Windows 则没有这种机会。为什么?因为当年这些技术被认为是「严肃计算 (serious computing) 」,圈限在服务器和高端工作站领域。当时 Microsoft 的判断是这些基于昂贵硬件的技术一时半会(也就是十来年吧)不会进入 PC 领域。所以,弃这些计算机工程领域的思想珍宝不顾,Microsoft 忙着在 PC 领域重新发明关于安全、多任务、多线程、图形等等功能。甚至在 Windows NT 4.0 中,为了考虑 PC 的「特殊情况」,把 NT 3.5 已经实现的而且被业界认为是正确趋势的用户态窗口管理系统搬到了内核态,开历史的倒车。另一方面,乔布斯离开 Apple 之后,给自己的创业定了一个奇特的目标 —— 创造最好的学术研究电脑。为此而打造的 NeXT 硬件系统真的是贵族血统,却也落得个血本无归的局面。但正是在这个贵族平台上,NeXT 有机会审视和改进那些 Microsoft 不敢问津的「严肃」技术。Windows 的「陈旧」和 OS X 的「新」不在于时间,而在于前者 quick-and-dirty 的战略和后者略显固执的坚持不妥协。

微内核领域的传说

2011/08/05

IT 业和自然科学领域常说的「传说」一词来源于英文 myth ,是个负面的形容词,更接近「流言」、「谣言」的意思。如著名的电视节目《流言终结者》(mythblaster) 。也经常被翻译成「神话」,如著名的《人月神话》(Man Month Myth) 。这篇文章不是要贬低微内核 (micro-kernel) 这个概念本身,而是说人们对这个领域中的很多东西存在不小的误解。

Mac OS X 是微内核

OS X 的内核叫做 XNU ,是一个基于 Mach 和 BSD 的内核。因为 Mach 是最早的微内核,所以人们自然而然地认为 XNU 也是一个微内核。在软件领域「基于」这个概念是很模糊的。甚至一个系统只是从另一个系统的设计中借鉴了一些思路,也可以称为「基于」。虽然我相信 Windows NT 和 VMS 没有一丝一毫共同的 code base ,但还是认为前者「基于」后者。XNU 确实是继承了 Mach 和 BSD 不少的 code base ,但 XNU 抛弃了 Mach 最核心的设计选择 —— 微内核。所以即便二者有大量共同的 code base ,也只能勉强称为「基于」。

为什么继承了 Mach 大量 code 的衍生产品 XNU 反而抛弃了被继承者的核心设计?这要回头看 Mach 是如何开发的。开发一个全新的内核是件非常困难的事情。在内核的基本功能完成之前,开发者的工作几乎都是不可见的。Linus 在《 Just for Fun 》里回忆,到第一个 shell 被成功移植到 Linux kernel 上之前,他都看不到自己的工作有什么成效,突然( shell 移植完毕),似乎一切一下子完成了。这种最后突现的情形,对软件开发来说风险很大。因为错与对要到很晚才能揭晓。Mach 的开发采取了另一种渐进的、随时可以检验成果的方式:

  1. 取一个完整的 BSD 内核,改名叫 Mach 。
  2. 在这个内核上开发一套底层 IPC 机制。一般 UNIX 的 IPC 机制会利用内核模块的功能,比如 I/O ,内存管理,等等。这在微内核上是不推荐的,因为像内存管理或者 I/O 这类功能在理想的微内核上要作为用户态 (user-space) 服务。所以微内核的 IPC 非常底层。在 Mach 上是基于 message 和 port 的消息队列通信。
  3. 把原来的 BSD 内核各模块间通信(主要是方法调用)替换为 message/port 通信。
  4. 把已经改为通过 message/port 通信的模块从内核态 (kernel-space) 移出到用户态。

XNU 的做法是 undo 了第 4 步(有时连第 3 步也 undo 了),保留底层 IPC「机制」(mechanism) ,抛弃了 Mach 的微内核策略 (policy) 。通过这么做 XNU 拥有一个独特的优势 —— device driver 可以采用两种状态来运行,运行在用户态的时候利于开发和调试,运行在内核态的时候又不会引入微内核的开销。前者基本上只用来提高驱动开发的效率,实际发布版的 device driver 总是运行在内核态。所以,OS X 的内核 XNU 不是微内核。讨论微内核的成功与失败,性能高低等等问题,OS X 都不是一个适当的例子。

微内核解决了 Monolithic 内核不能解决的问题

往往认为微内核解决的最大问题是 monolithic 内核的复杂度和可维护性问题。可实际上微内核和 monolithic 内核在复杂度上并无显著差别,甚至后者还简单一些。从 Mach 和 OS X 内核 XNU 的例子可以看到,加上第 4 步,得到的就是微内核。去掉第 4 步,得到的就是 monolithic 内核。去掉第 3 步,得到的就是一个进一步简化了许多的 monolithic 内核。进一步去掉第 2 步回到原来的 BSD 内核,仍然是一个设计良好的内核,这也是 Mach 能在其上循序渐进的开发成功的原因。

其实,代码运行在内核态还是用户态不是构建其上的系统整体复杂度的决定因素,决定整体复杂度的关键在于各部分代码之间的功能边界是否清晰。微内核的作用在于强制约束了各个功能之间的边界,并且提供了有限的保护,也就是利用硬件 MMU 进行内存保护(但是这种保护并不真的总是有效,由于模块的 bug 造成的 crash ,常常并不能仅靠重启该模块来修复,而是必须重启整个微内核和所有服务,所以一个单独模块在内存保护之下 crash 所造成的危害并不见得一定比整个内核 crash 来的小)。这种约束的代价并不低廉。实际上,微内核本来的雄心是把 task schedule 和 IPC 之外的功能都放到用户态,但是由于硬件设计的限制(很多硬件其实都是按照 monolithic 内核的需求设计的),性能因素,以及内存保护能提供的实际错误恢复能力,实际中以微内核自许的产品 —— 比如 MINIX —— 一般也只能把 device driver 这样的外围模块放到用户态。提供强制的设计边界固然是好的,但是只有开发社区达到很大规模,这种需要付出代价的强制边界才能真正带来价值。小规模的开发社区更容易自律,从而更好的发挥 monolithic 内核本身结构精简的优势。而 device driver 的开发从来不是一个从业者人如潮涌的领域。

另一方面,monolithic 内核并不是真的铁板一块。Monolithic 的定义是不「强迫」开发者把模块放到用户态。「不强迫」不是「强迫」的对立物,而是后者的「超集」(superset) 。Monolithic 内核并不会阻止开发者把模块放到用户态,它所要求的是开发者自行开发一个小规模的内核态 stab 来负责把用户态模块的操作 relay 到内核。FUSE 就是一个在 monolithic 内核中实现用户态 driver 的例子。

所以微内核只是一个试图规约开发者的规范。它能解决的问题,并非 monolithic 内核所无法解决的,只不过侧重的灵活度角度不同。

微内核的效率很低下

这是个很有争议的问题。有些微内核将处理系统调用 (system call) 的模块也做成了用户态服务(仅仅是处理系统调用,而不是这些调用真正代表的功能。这里的处理系统调用模块只负责把用户态应用的请求分发到实际的功能服务)。即使是一次什么都不做的空系统调用,也要导致四次上下文切换 (context switch) 。

  1. 从发起系统调用的进程,切换到内核态。
    (只有栈和寄存器等状态切换,不需内存映射表的切换)
  2. 从内核态切换到处理系统调用的服务。
    (既有栈和寄存器等状态切换,也有内存映射表的切换)
  3. 从处理系统调用的服务切换回内核态。
    (只有栈和寄存器等状态切换,不需内存映射表的切换)
  4. 从内核态切换回发起系统调用的进程。
    (既有栈和寄存器等状态切换,也有内存映射表的切换)

常见的 monolithic 内核一般只需两次无需内存映射表的切换的上下文切换( 1 和 3 )。而且,由于处理系统调用的模块往往并非真正提供服务的模块,所以对微内核来说非空的系统调用会有另外的两到四次切换,而对 monolithic 来说只是一些普通的函数调用。作为 monolithic 内核 XNU 无需切换 2 和 4 ,但《地址空间划分》中介绍过,由于比较独特的地址空间,需要在切换 1 和 3 时切换内存映射表,所以开销会稍大。有 benchmark 测试曾经以 XNU 处理空系统调用慢于 Linux kernel 来说明微内核的效率低下,其实是误解了这个细节差异。

支持上面这个理论分析的实际性能数据大多都是在 Mach 微内核上得出。但是有人指出 Mach 是微内核的第一次尝试,设计和实现失误很多。比如很多的性能开销 (overhead) 其实发生在消息的合法性检验操作上,而非上下文切换。特别是用 Linux kernel 改写而成的 L4 微内核的支持者认为 L4 更好的精简了消息 IPC 机制,更多的利用了 x86 架构的独特硬件特性,性能和 monolithic 内核不相上下。

在我看来,这是「 Java 的性能已经和 C++ 不相上下」的另一个版本。不论怎么说,微内核引入的开销是客观存在的,更多细心精巧的设计也只能是降低这个开销,不可能完全消除。而微内核的安全保护,只有当内核的开发社团拥有一定规模的时候,才能体现出其价值。可是作为核心底层的模块,内核和 device driver 的开发永远是一个小团体的世界。特别是在 Mac 和 mobile 这类软硬件结合的生态体系重新获得成功,替代了以 multi-vendor 为特征的 PC 生态体系之后。

Premature Optimization

2011/07/26

昨天在知乎上看到有人问某领域的基础学习是否可以有所侧重裁剪的问题,第一反应是:学习基础得不能再基础的东西还裁剪什么?由此不由得想到了简化字。从有草书和行书开始,简化字的存在其实也有几百甚至上千年了。建国后的汉字简化过程,其实只不过是正字废除过程而已。丢弃的知识不说,语言本身在某些方面反而被复杂化了,有些本来可以通过单字明了的意思,比如船只的「隻」和只有的「祇」,现在必须通过上下文分辨。

软件开发领域之外无数草率的裁剪,和软件开发的「 premature optimization 」本质上属于同一种行为,拥有共同特征。第一是基于假设的而非真正的 hotspot 。软件开发的 premature optimization 不经过 profiling ,所以经常发现辛苦优化的代码在使用中很少被调用,优化所得的效率提高完全被边缘化。简化字号称能加快汉字认读,其实以汉字认读为主的学习阶段只有两年左右(还是在兼学其它科目的情况下),而现代中国人以学习为主的过程持续九年到三十年以上。简化汉字牺牲文字带来的收益其实得不偿失。

最成功的软件系统是那些最终用途超乎设计者想像的系统,像 UNIX 操作系统及其各种工具,C 语言,Photoshop 等等。Premature optimization 大大降低了这种可能性,让系统能顺利经受预料之外情形的能力低下。至少,在外界情况发生变化时,premature optimization 所允诺的优势的往往烟消云散。今天学简化字成长起来的这一代人,认读正体字毫无困难。这是当年行书和草书的设计者们在长年积累中对认读心理的直观认识的结晶。这说明在认读方面正体简体并无区别。那么正体废除运动的优势似乎就只有书写一路了,偏偏今天的汉字书写又计算机化了。人类永远不知道自己预言未来的能力有多差,而勇于否认这点的莫过于 premature optimization 的施行者。

好在当年汉字拉丁化这个终极优化方案被否定了。这里不是要悲叹正体字在神奇大陆的消失,而是要借此说明 premature optimization 的本质邪恶。优化是我们可以清谈的东西,是可以做理论研究的东西,是可以边边角角尝试的东西,但绝不是指导下一大步的方针。

黑客的闲暇

2011/07/12

有时候空闲下来不知道该干些什么。这时要是能找到一个有意思的问题来研究会感觉很充实。若面对的不是纯粹知识理解的难题而是一个有实际需求的问题,并且又能做出点什么东西满足这个需求,那心情就更好了。但更多时候是,即使闲着难受,也会觉得时间太短不够解决意料之中和意想不到的困难而难以鼓起勇气真正动手。

上周一整周都在休假,其中不少时间花在读剩下的少半本《 Snow Crash 》上。这本书断断续续读了一年半的时间。这类小说让我兴奋的地方在于看过之后,再面对普通工作时感觉那些东西多多少少带了一点向 cyber 世界慢慢前进的味道。主人公 [1] 用代码构建和打破 Metaverse 的规则或者拿着电脑 hacking 是我最喜欢的情节。讲到 hacker 和 hacking [2] ,作者 Neal Stephenson 是个行家,虽然这是本带有很大政治和文化成分的科幻小说而不是像《 Hackers & Painters 》那样的专门讨论 hacker 文化的文集,而且是在 1992 年的 IT 石器时代 [3] 写成,但是其中蕴含的很多 hacking 的精髓是这两年我才渐渐了解的。这是我近一年来越来越喜欢这本书并且决心在一周里读完剩下半本的原因。

在小说里最后一场大战开始之前,Hiro Protagonist 乘坐的 Mafia 游艇被 Raven 所在的潜艇击沉,他们一行四人的救生艇飘荡在太平洋上。一个无聊的 hacker 会做的自然是 Hacking !

Hiro does not know what he is doing, what he is preparing for. That’s okay, though. Most of programming is a matter of laying groundwork, building structures of words that seem to have no particular connection to the task at hand.

Metaverse 是个三维的虚拟现实世界。不过在救生艇上,为了延长电池续航,Hiro 特地进入了自己的纯二维工作空间 Flatland。让我觉得自己为了节省电池在 09 款的 MacBook Pro 上 re-login 的切换到集成显卡是个挺酷的举动。读完整部《 Snow Crash 》,我最喜欢救生艇上这段情节。因为背个笔记本电脑到处乱跑就是我在大学读书就有的梦想。写完一个简单的工具之后,Hiro 开始准备在 Metaverse 里最重要的工具,一个『隐身』的 avatar :

To write a really good invisible avatar from scratch would take a long time, but he puts one together in serval hours by recycling bits and pieces of old projects left behind in his computer.

什么都从头写起也许是有些人刚出道时的野心,不过对大多数干过几年的程序员来说,有东西能被『 recycle 』并且能用这些零碎『 put one together 』才是真正的资本。所以,在有时间有精力的大项目里要 leave behind 什么东西,才能有朝一日在『救生艇』上 put together 一件趁手的兵器?这是个严肃的问题。

除了看书,休假的这一周还受朋友之托要写个简单的小工具。我没有大块时间可用。倒是如上所讲,有些时间看小说,不过休假前半个多月的工作到了这样的程度,那些时间不用来看看小说其实也支撑不了什么脑力活动。但答应的东西要做,好在不是什么和 Apple 竞争的商业界面。我决定用而且只用 Adobe Bridge 的 ExtendScript [4] 来搞这件事情。编码和读小说是交替进行的,当读到 Hiro 救生艇 hacking 这段的时候,小说和现实仿佛重叠了。最后我终于 put together 出一个还说得过去的东西。作为正式工具来说太粗糙了。我觉得很大一部分原因是以前从未真正考虑为这样的『 put together 』leave behind 什么像样的东西。

我考虑从现在开始应该为 leave behind 做点什么准备。看来最终在『救生艇』上 put together 一定是用『高级动态语言』来完成。但太粗糙的东西不行。最后用『高级动态语言』拼接的『 bits and pieces 』必须是称手的、精致的。所以得用『集成污染』最少的语言 [5] 打造最好的模块。所以,我要再深入的学习 Lua ,而且开始考虑给我的所有 code base 加上 Lua 集成。

脚注:

  1. 主人公就叫 Protagonist。
  2. 这里的 hacking 和 security 没有直接关系。是《 Hackers & Painters 》里说的那种 hacking 。
  3. 1990 年的时候父母要给我买台电脑,我为他们乱花钱感到极大的惊恐。1993 年我有第一台电脑,以为要解决问题必须用 BASIC 来编写程序。
  4. 一个 JavaScript 的定制版本。和普通的 JavaScript 不同,ExtendScript 运行在 Bridge 而不是 Web 浏览器中。DOM 也和一般操作 HTML 和 CSS 的 DOM 完全不同。它基本上是用来编写桌面 UI 的。
  5. 集成污染

容忍度的拐点

2011/06/13

For the average user this new kind of [Web] software will be easier, cheaper, more mobile, more reliable, and often more powerful than desktop software.

—— Hackers & Painters, The Other Way Ahead

The intended role for the Macintosh was as a client operat-ing system for nontechnical end users, implying a very low tolerance for interface complexity. Developers in the Macin-tosh culture became very, very good at designing simple interfaces.

—— The Art of UNIX Programming

我有个朋友最近忙着补救项目中走弯路浪费的时间。在项目早期,他们的 team 里有过一次大争论:UI 是全部采用 native 代码还是部分采用 Flash 。最终的结果是 Flash 一派占据了上风。理由是大客户相信 Flash 的『成功案例』。当然,像我朋友这样做过『消费者』应用的人认为所谓大客户的意见都是『企业开发』的狗屎。

事情的发展是,虽然 Flash 方案确实比 native 方案有更多开发便利(比如更方便的内存管理,更可靠的 exception 处理,以及从上一个『大客户企业方案』中继承下来的部分 code base ),但是在这个项目里反而拖累了开发进度。整个 UI 没法形成一个整体。在早期争论中被祭出的『大客户』也渐渐让位给习惯了 Apple 产品和 Photoshop 之类软件界面的挑剔消费者。走过一段弯路之后,整个项目重新回到纯 native 方案。

这让我回想起 2001 年前后,我的程序员生涯中最『惬意』的一段。因为客户很『 resonable 』,我可以经常告诉他们这个或者那个『没法做』。为什么呢?因为客户相信 Java ,他们的整个 IT 基础建立在 Java 上,他们要求我们用 Java 。所以,当你证明了 Java 不能做一件事情之后,你就证明了宇宙的最高法则。所以,是这些家伙自己放宽了自己的容忍度。

最近有一本 Paul Graham 的作品集《 Hackers & Painters 》在国内出版,网上讨论和引用很多,总体来说这是本不错的书。其中第二篇《 The Other Way Ahead 》把 Web 技术描述为软件开发未来的趋势。如果审视一下这个『未来的趋势』,我们会发现同样的问题。你想关闭某个文本框的拼写检查,而在另外一些文本框里打开吗?Web 做不到。如果你无意中按住鼠标拖过页面,划过的文字可以不被高亮选中吗?Web 做不到。希望你的『 Web app 』支持 undo/redo 的快捷键吗?Web 做不到。你的 Web app 可不可以有一个真正的『护栏』防止用户做某些从任何意义来说都不合时宜的操作而不是仅仅规劝或者提醒吗(比如点浏览器的 Back 键)?Web 做不到。 ⋯⋯ 当然,用户不会因此特别不满意。他们会说:这是个网站,有什么办法。我经常听到有 Web 开发者说:我们能掌控整个 server ,所以我们可以自由选择合适的高级语言来完成任务,而不像可怜的 native 应用开发者。他们错了,是用户放弃了对他们的应用的品质的要求,用户认为那是个网站。

作为单独 app 的 Google Earth 和作为『网页』的 Google Map 有什么不同?那就是下面这种处理网络延迟的情形在任何一个 app 的 beta 版里都是不能容忍的:

面对这些问题,乐观的 Web 开发者经常说『情况已经不是那样了』或者『马上就不是那样的』。是啊,从 canvas 这样的机制引入 HTML5 可以看出 Web 提高用户体验的努力(但是暂且别说 canvas 的支持目前有多么脆弱)。可是,用户中最挑剔的那部分的容忍度也是变化的。以 version control 这个程序员的工具来说,以前字符界面就很好,今天很多人在用矢量图界面来处理复杂的 branching ,也许明天就会有 3D 化的 UI 。软件的一个基本要求是根据用户操作改变界面的状态,比如在 Word 里改变一下行宽,或者在 PowerPoint 里修改阴影,或者按 Cmd+T 让 Firefox 创建一个新 tab ,用户一开始对这些变化没有过多要求,所以软件都是从上一个状态突变到下一个状态;然后,用户开始要求在状态变化中显示一些重要的中间态;今天,大多数变化都要求用平滑的动画显示。而 Web 技术在追赶这些不断收紧的容忍度时,其实已经失去了一部分作为『未来趋势』的优势。比如,所谓 watching users 的优势,只对于像 CGI 那样每个操作触发一次 client-server traffic 的应用才有用。为了追求用户体验而不断引入 client-side script 和 AJAX 的 Web ,在这方面早就慢慢和 native 应用拉平了。

回顾技术的发展,其实没有什么焕然一新的方案能够在不牺牲用户体验的前提下大幅度减轻程序员的工作。那些所谓的快速开发手段,只是用 PR 攻势来催眠用户放宽容忍度。这个是 cross-platform 。那个是『云』。用这些概念来降低用户的期待。当然不否认,某些技术提供了一些当时用户急需的便利,可以认为它们要求用户放宽容忍度的方式在一定期限内是合理的。关键在于,开发者往往看不到用户为便利付出的代价(而且常常是程序员独有的便利)。当一些开发者抱着用户和开发者都获得了『免费』便利的错觉时,这种技术就成了他们眼里『未来的方向』(这个词是我从《 Hackers & Painters – The Other Road Ahead 》某些段落的字里行间中读到的。当然《 Hackers & Painters – The Other Road Ahead 》这篇文章里有很多观点是建设性的,比如对软件演进和技术支持和对团队士气的看法。对 Web 过于乐观的态度,考虑到其写作的时间也情有可原。只能说,如果作者到今天还是这些观点,就略显自大了)。开发者要避免这种自大,要清楚自己和客户为了一个方案各自付出了什么,用户何时会失去容忍度。失去这种自知之明是某些 Web 时代的人面对 native 程序回归时表现出吃惊的原因。

机械打字机

2011/06/04

纸质笔记本和机械打字机

高级动态语言与软件业 —— 交流与内省

2011/06/01

一直以来,我都认为编程语言是程序员之间的语言,是人和人交流的工具。主流编程语言最重要的特性是清晰、准确,开发者个人在使用中是否感受到的灵活、简洁、优雅则是次要的。这是把编程语言和同为交流工具的自然语言比较,同时又因为两者构建文化认同的程度有所不同,所得出的结论。

接触高级动态语言也已经有近四年,一直觉得虽然它们并不适合作为程序员交流的工具,但绝对是程序员的工具箱里不可缺少的一样东西。几个月前写了一篇《高级动态语言与软件业》,主要观点是,语言概念相对简单,用法相对固定的静态语言更适合构建商业级别的产品,而高级动态语言只限用于符号推导系统。之后是对 Lua 的学习和《集成污染》。高级动态语言并非我所能认同的交流工具,但又具有不可否认的必要性,我经常重新考虑『编程语言/自然语言』这个类比应该如何涵盖高级动态语言。

今天和别人讨论 Objective-C  vs. C++ ,突然回忆起到半年前看的《 What Technology Wants 》里对自然语言的描述:

The creation of language was the first singularity (注1) for humans. It changed everything. Life after language was unimaginable to those on the far side before it.  …  But the chief advantage of language is not communication but autogeneration. Language is a trick that allows the mind to question itself; a magic mirror that reveals to the mind what the mind thinks; a handle that turns a mind into a tool.

这时我想到自己从未把自然语言的『自省』功用和编程语言联系到一起。也就是上文引用里的所谓『 question itself 』,『 reveals to the mind what the mind thinks 』。如果把『自省』这个元素考虑在内,『编程语言/自然语言』这个类比就更完整。复杂的产品中用户定制和核心算法的符号推导部分更加接近『自省』而非『交流』。而且,很多有经验的程序员都说过,你不一定要用它来开发产品,但是学习一门高级动态语言能让你更了解编程的本质。这和自然语言的『自省』功用更接近。最后,自然语言的『自省』功用大于『交流』,编程语言的『自省』功用也很重要,但应该退居『交流』功用之后。因为毕竟程序员不是机器,他们的思考大部分还是通过自然语言完成的。软件产品也不是机器人,很多功能进化依然是通过用户反馈、迭代开发加更新发布而非简单的定制完成的。

脚注:

  1. 顺便说一下,在读到这段文字之前,尽管我知道『技术奇异点』之后和之前就像人和动物的区别,但是我从来没直接意识到自然语言就是史前的奇异点。

集成污染

2011/05/27

写过《高级动态语言与软件业》之后,Alex 回复说 Lua 语言应该符合我的期望。其实我应该早就意识到 Lua 的存在,因为 Lightroom 就是用它作为 policy 部分的编程语言。只是 Lua 的库不如 Python 等语言强大,作为编写应用程序的主力语言还显单薄。但正是因此使用轻量级的 Lua 的开发者很少受到诱惑用它来开发复杂算法和 policy 之外的东西(比如 UI ),那正是我在《高级动态语言与软件业》里所反对的。

另一方面,作为描述算法和 policy 的动态语言的另一个重要条件在《高级动态语言与软件业》里讨论不多,却正是 Lua 的优势 —— Lua 是我所知的造成『集成污染』最少的库。

什么叫做『集成污染』?简单地说,如果系统的构建( build )在加入某个模块之前一直好好的,加入这个摸块之后就开始出现问题,这就叫集成污染。这种症状在编译、链接和运行阶段都可能发生。例如,编译阶段的集成污染的可以来自 C 的 macro ,typedef ,条件编译,C++ 的 template 。运行时的集成污染可能来自不同的多线程模型混用。

一个模块能否被顺利地集成到更大的系统中,取决于它希望别的模块如何对待它。一个模块对其它模块的要求越多,越不明确,集成的困难就越大。当这些要求被错误地理解或者无法满足的时候,就变成了集成污染。如果一个模块只是对数据的类型有所要求,那么任何一个可以定义抽象数据结构的静态语言都能很容易的描述这种要求,而且也很容易得到满足。但是,即使是如此简单的需求,也会遇到命名冲突之类的问题。通常这就是 macro 和 typedef 造成集成污染的原因。

接下来,有些模块的要求在『理论上』能够被满足,但是这种要求超出了现有技术的表达能力。C++ template 和多线程就是这样的例子。一个使用了 template 的模块很难用 C++ 语言本身的结构来说明自己的需求。对多线程来说情况就更严重,因为不仅编程语言不能描述线程模型,连数学语言和自然语言也很难清晰描述一个模型对线程使用的期望。所以,基本上集成涉及多线程的模块是一种盲目试错的过程。

更进一步,有些模块的要求是否能被清晰地描述已经不重要,因为这些要求常常是难以满足的。这些模块要求独占 system-wide 稀缺资源。比如,Cocoa 框架把整个应用的很多初始化工作和主消息循环封装到一个黑箱函数 NSApplicationMain() 中,古老的 C++ Framework 如 MFC 也会隐藏 main() 函数。这些库用自己的方式任意使用消息循环和主函数。如此霸道的模块,在系统中是一山不容二虎。《高级动态语言与软件业》提到很多高级动态语言和静态语言的互操作方式仅限于单向发起 —— 能调用静态语言编写的动态链接库,但是不能作为库被其它(静态)语言调用,这就等同于对主函数(有时连同消息循环)的独占。

解决集成污染的方法是各种边界分离。像 macro 、typedef 之类的命名污染可以通过文件分离来解决。终极方法是进程分离加 IPC 。不论如何难缠的集成污染也会在进程边界止步。另一种解决之道是参与集成的模块低调、收敛、谦虚一些。这就是 Lua 的方式。采用 ANSI C 中最成熟的跨平台部分,避免了编译时的错误。放弃对线程和 I/O 的支持,避免了运行时污染。Lua 能做这样的取舍,原因在于有合理的目标,把自己定位为描述算法的符号操作语言,和 hosting 应用的交互限于严格而且有序的符号集合。这让 Lua 成为扩展 C 语言和的方案中最简洁和最可靠的形式。