美国归来

2011/05/22

在美国参加了一周的公司 Tech Summit ,之后几天做了一些项目上的交流。这是工作以来到国外出差最愉快的一次。其中的原因是因为所在的公司不同,所工作的产品不同,交往的人不同,也因为我自己和以前不同。美国的工程师和经理都非常友好而且优秀。其中两位华人更是在工作中充满活力和激情,而且为人豪爽。

除了大基调比较愉快之外,还有一些小细节准备得比较成功。

首先是一定要带自己的 laptop 。每天不能睡自己的床,不能盖自己的被子,不能待在自己的书房,不能用自己的专用浴袍,这些本来就是很不爽的事情。如果再加上不能在熟悉的 laptop 上阅读和娱乐,那整个行程中几乎就没有任何放松的机会可言。所以,即便回了酒店只是用 Safari 上上网,也必须打开自己的 laptop 才能算真正的放松。更何况在这边买了《 StarCraft II 》,如果不在自己的 laptop 上也有诸多不便。所以,千万不能偷懒只带公司的 laptop ,我曾经有三次出差不带自己的 laptop 的经历,都不太愉快(当然也不是带了就一定愉快,还有很多其它因素)。我还曾经把公司的和自己的两台 laptop 都作为随身行李,结果旅途中和安检的时候都非常累。这次把公司的 laptop 随托运行李,所以很轻松。

其次是一定要早睡早起。这样白天精神好,应付工作不会觉得压力大。特别是从开会到中午吃饭聊天开玩笑都必须全程英文的时候,这种压力甚至稍稍超出了工作的范畴。早起争取的时间很珍贵。如果不是单独出差,那么同行的同事计划安排之间的一些冲突和摩擦是一种额外的无形压力,每天从别的同事起来的那一刻就扑面而来 —— 打到你房间的电话、在 lobby 商讨出行和会议的安排等等。比其他人都早起几个小时是一种非常大的优势,可以不受打扰的观察和思考全天的安排或者决定打扰别人(而比其他人都晚睡几个小时是得不偿失的,甚至是不可能的)。

最后,从睡眠的另一方面来说,越早消除 jet lag 的影响,也就拥有越大的优势。所以从踏上启程航班开始,就要严格按照目的地时间安排睡眠。

18 日清晨,步行去公司的路上。准备喝杯咖啡然后把停在公司停车场的车开到酒店。离回国的航班起飞还有 6 个小时。

准备启程

2011/05/06

度过了异常忙碌的一周。后天就要去美国参加公司的 Tech Summit 了。晚上把包收拾一下。这个是我自己的 MacBook Pro 。明天还要用内胆包带一个公司的 MBP 。如果只拿公司的本子,就会觉得晚上过的很没意思。

无毒副作用

2011/04/21

每当提到如何『评价』面向对象技术时,很多人的直接反应就是上升到世界观层次。说它让软件设计和客观世界更好的一一对应云云。我非常怀疑这种思考方式。客观世界是多维度的,面向对象的软件系统只能选取一个维度进行描述,同时为兼顾其它维度要做各种 leaky 的妥协。因此,对一个客观场景总有很多个可能的面向对象『设计』存在,每种『设计』都不能被认为是错的,甚至不能被认为是差的。这种一对多的情景很难让我信服面向对象技术比其它手段能更『自然地』描述世界。对我来说『自然地』只有一种标准 —— 容易取舍的单一选择。

面向对象技术没有脱离面向过程和命令式( imperative )的思维。有经验的 C++ 、Java 、Objective-C 程序员在阅读和编写代码时能熟练到近乎反射地把 this 或者 self 作为一个参数来思考(比如 0->func() 这样的调用为什么在某些情况下是可行的),或者把函数 override 作为函数指针的另一种形式(比如 0->func() 为什么在某些情况下是不行的)。这里没有任何世界观的升华。面向对象真正的便利仅限于消除负面副作用( negative side-effects )。

举个例子来说明什么叫负面副作用。假设在程序中有个二维的『单位向量( normalized vector )』,其方向任意,长度必须为 1 。这个数据通常表式为 x 分量和 y 分量。作为单位向量,必须满足 x 和 y 的平方和始终为 1 ,这种条件叫做对象数据的不变式( invariation )。不小心打破不变式,比如把 x 和 y 都设成了 1 (平方和变成了 2 ),这种行为就叫负面副作用。在数据暴露给所有算法代码的编程方法中,不变式是通过文档由程序员人工保证的。面向对象方法认为人工保证容易出错,所以通过隐藏对象的数据,只允许有限的成员函数访问它们来保证不变式。这样需要精心维护的涉及不变式的代码仅限于少量成员函数。类似的例子还有如何表示一个钢体(内部没有复杂结构,各部分的相对位置不会变化的物体)在三维空间的位置和姿态。钢体本身的形状一般用一组顶点表示,它在三维空间的位置和姿态用一个 4 x 4 的变换矩阵表示。熟悉 3D 几何的人知道,4 x 4 矩阵可以表示拉伸、平移、旋转、甚至扭曲等任意的变换。但是对于钢体来说,只有旋转和平移是有物理意义的。所以必须对这个数据结构中的 4 x 4 矩阵做出限制,即钢体对象的不变式。

从另一个角度来看,面向对象保证不变式的方式未必是理想的。首先,人工约束不变式并非那么难。Linux kernel 核心数据结构的不变式就是通过活跃的讨论和 code review 保证的。其次,人工约束不变式可以让设计者天然地保持警觉,把核心数据结构的数量最小化。最后,不变式可以通过合理的设计数据结构来减小甚至消除。比如上面单位向量的例子里,可以把单位向量用一个夹角 —— 即它和坐标系的某个轴的夹角来表示。这样的表示法不再需要额外维护不变式。类似地,在钢体的例子里用一个平移向量和一个旋转向量代替 4 x 4 矩阵表示位置和姿态,可以减少要维护的不变式。

这两个例子都说明,不变式来自数据结构的信息冗余。当然,表示复杂的对象很难做到完全消除信息冗余。若底层 framework 的 API 直接提供 4 x 4 矩阵运算或者 (x, y) 二维向量运算,我们就要考虑在这样的系统上构建对象的简便性和复杂度,未必要舍近求远用平移、旋转和夹角来表示。同时,保持冗余信息也是提高性能的手段,很多操作都需要 x / y 分量或者 4 x 4 矩阵,如果不直接存储它们,就要在每次需要的时候临时计算。所以造成不变式的信息冗余又来自两处:一是性能优化的需要;二是支持我们构建对象的底层系统中可以被直接操作的模块粒度不够小。这就像现实中我们没法构建一个无需仓库的物流,也无法用纳米亚原子直接打造机器。

前面提到用非面向对象技术消除副作用并非难如登天。另一方面,复杂系统的信息冗余不可能完全消除决定了面向对象技术多少是有用的。这里的结论是不要把一种消除副作用的便宜之计吹捧成改变世界观的灵丹妙药。特别是在一叶障目之后,容易忽视背后的更本质更有效的手段。很多吹捧面向对象技术的人往往喜欢把系统的核心数据结构设计的非常庞大,信息非常冗余。从编写复杂的保持不变式的代码中获得不真实的成就感。这种过度吹捧在面向对象以外也有例子。并行化技术最大的负面副作用是不同线程同时修改一份数据,即 race condition 。消除这个副作用有几个方法,其一是被诟病的 lock 技术。Lock 的问题在于为了试图保证数据的不变式引入了时序上需要额外保证的不变式。在《多核与锁》里有所讨论,这里不过多涉及。

接下来是两种直接保证并行处理中数据不变式的手段。一个方法是只用栈变量,因为栈是每个线程独立的。更精确的说是只使用 per-thread 数据,因为有些 per-thread 数据其实在全局堆里,只是通过 per-thread 的引用来管理,而且后面马上说到有些系统的 per-thread 数据不是基于栈。这方面有一个最近成为热门的例子 —— GPU 运算。OpenGL 的 GLSL( shader language ),CUDA 和 OpenCL 的编程语言处理的数据都仅限于当前 GPU core 使用的 per-thread 变量。另一种方法是根本不用变量,那就是一小撮人推崇备至的函数式编程( functional programming )。在《并行计算的解药》里我讨论过,前一种手段里分割输入数据才是关键。而后一种,我认为根本是步子迈的有点大,扯着了。对这两种手段的评价,都只能限于消除副作用的影响,解决问题的真正关键在于更高层次的数据分割。

一项技术技术是否能被主流软件开发社区采用,不在于世界观和哲学那类东西,而是更实际的 —— 在消除副作用的同时保持原有的思维方式;至少增加的额外思考不能超过熟练程序员的反射弧。要求改变世界观的技术一开始就失败了一半。

土气

2011/04/07

自从成为 Mac 用户之后,我一直非常想把 Firefox 从系统中清除出去。原因有三个:第一,我基本不用 add-on ,偶尔重度使用的几个功能也都有 Safari extension 或者 Chrome extension ;第二,从 code base 的角度说,我对 Webkit 的信任度大于 Firefox ;第三,Firefox 太『土气』。前两条我认为尚属个人感觉。这第三条从道理上说主观的不能再主观,可我相当肯定这种感觉。和 Safari 相比,Chrome 经常给我欠修饰,不圆润的感觉,但绝对没有『土气』。可看到 Firefox 就和看到上世纪 80 年代风格的服饰一样,而且是那种绝不会借复古卷土重来的土气。

最近发现一例,是对正在下载图片的处理(注意这里的例子是正在下载但还没有结束也没有彻底下载失败的图片)。Safari 是直接留白(强调一下,Safari 对彻底下载失败的图片的处理并非留白)。

Firefox 的处理是从上世纪不变的黑框加『圆、方、三角』图标。

这两种效果有什么区别?不用讲太多道理,第一感觉就是 Firefox 的效果太『土气』。我和同事曾经比较过一些应用的界面。一个可拖动的分割线( draggable separator )是单像素宽的黑线还是多个像素宽的伪 3D 效果,这点区别就能决定一个界面是整洁还是凌乱(猜猜哪种让界面更整洁)。这种感觉不易量化,但绝对客观存在。不管有什么理由,『土气』就是一种缺陷,就意味着你得花额外的精力从其它方面弥补。

Firefox 的『土气』带来了什么收益?它告诉了用户一个在今天的网络状态下持续不会超过两秒钟的状态(为了抓上面那张图我费老了劲了,真想找个让网速临时慢下来的工具)。去掉这个状态的可见性不会令用户丝毫不便,刻板的显示这个状态给用户带来的只有一闪而过的『视觉污染』。就这个例子来说,Firefox 的设计不仅仅是土气的问题,它忽视了 UI 设计的一个基本原则,不要浪费用户的注意力。用户的『注意力』是稀缺资源,这种知道不知道两可的程序内部状态无需让用户费心。这是 Firefox 对拨号时代的遗产长久不加审视的体现。我不敢说从这个例子可以总结出什么一般规律,但是我怀疑那些表面上看着土气的设计也总是隐藏着对可用性的某种深层次的忽视。

逻辑的残影

2011/03/31

作为一个棋力不高,但尚可作为消遣的普通人,我认为棋力的高低体现在头脑中能预先演算多少步棋。不过,听说即使是高手演算时也常会犯一种错误。举例来说,当演算到第三步的时候把一个子移开,但是演算到第五步的时候会下意识觉得那个子还在第二步的位置。这叫做『残影』棋子。

在做编程这样的复杂工作时,也经常在逻辑概念的变换中构建『残影』。最近就因为一个『残影』两天没有睡好觉。先不说犯错的经历,讲讲相关的概念本来应该是什么样子。计算机图形系统的终极目标是生成二维图像,这个图像可能会显示在显示器上,或者存储成文件,可能是一幅静态图片,也可能是一组图片构成的动画,可能是一幅照片,图案,也可能是一个虚拟 3D 场景的影像,总之终归是一个(或者多个)二维图像。生成二维图像需要预先开辟绘制的空间。这种空间一般显卡的显存中创建,因为这样才能使用 GPU 硬件加速。OpenGL 里对这种空间的抽象叫做 Framebuffer Object( FBO )。

我在《 OpenGL 随想》系列里说过 OpenGL 是一个重度依赖 context 的系统( Cocoa 的 Quartz 和 Windows 的 GDI+ 也依赖 context 。Quartz 的概念模型来自 PDF ,所以文档和打印系统也依赖 context 。由此可见一切图形相关系统都建立在 context 这个基础概念之上)。Context 这个概念有多重要,多基础,就体现出我后面犯的那个错误多愚蠢。FBO 是 OpenGL context 中的一部分,一个 context 可以拥有多个 FBO 。其中 ID 为 0 的 FBO 用于屏幕显示。

如果严格如上所述,那么这个模型是优美的。可是,OpenGL 设计之初只考虑到屏幕显示,FBO 在 OpenGL 2.1 和之前都不是标准的一部分,只是一个扩展( FBO EXT )。所以屏幕显示并不是真的被称为 0 号 FBO 。创建所谓『 0 号 FBO 』的过程和创建其它 FBO 的过程大不相同,所需的参数都直接传给创建 context 的函数。就是这个陷阱构建了逻辑『残影』的陷阱 —— 我把 context 当成了对『 0 号 FBO 』的封装。

正在开发的程序对 OpenGL 的应用都是 offscreen 渲染。所有的 onscreen 显示都在 OpenGL offscreen 渲染之后用其它方式完成。因为开发刚开始的时候在 Windows 平台上没来得及搞清如何使用 FBO EXT ,所以在 Windows 上的 offscreen 渲染也用的是『 0 号 FBO 』( Mac OS X 版本从一开始就使用了 FBO EXT ),必须创建一个不可见的窗口并将其 DC 句柄作为『 0 号 FBO 』传给 wglCreateContext() 。由于『 0 号 FBO 』和显示硬件紧密绑定,所以对高级需求有一些不可接受的限制。比如,普通 PC 显示器的单个色彩通道深度不超过 8 位,用『 0 号 FBO 』就没法生成 16 位色彩深度的位图。这时我开始在 Windows 上研究使用『真正的』FBO EXT 。

工作了一夜之后,我开始认为『真正的』FBO 解决了问题,而最初构建的那个逻辑『残影』从此开始作祟。因为把 context 当成了 『 0 号 FBO 』的封装,同时有了『真正的』FBO,我毫不犹豫的删掉了创建 context 的代码,包括在 Windows 上创建隐藏窗口和调用 wglCreateContext() 的代码,和在 OS X 上创建 CGL context 的代码。

结果当然是悲剧了。为了让过程更悲壮,删掉创建 context 代码之后的程序居然在一台 Windows PC 上和所有 Mac 上运行正常(除了一个不怎么被测试到的 case )。这个结果当然让我的『 context 不过是 0 号 FBO 的封装』的歪曲理论更加坚固。随着程序在其它 Windows PC 上产生垃圾结果,以及那个失败 case 被发现,我的头脑也跟着崩塌了。早上还在和同事夸耀删掉了创建 context 的『无用』代码,晚上就陷入了呆滞。

第二天早上醒来,迷迷糊糊中突然想起了什么是 context 的真实含义。早晨同事见到我的第一句话是『我觉得是删掉 context 的缘故』。知道这件事的美国同事也来信说他猜想是 context 的问题,尽管他只看到了运行结果没有看过代码。看来大家晚上都在思考啊(虽然美国是白天)!

回头分析,凭空构建出『残影』固然在于我因为时间紧迫没有仔细思考概念,但是很大程度上也是因为 OpenGL 的 API 由于历史原因没能清晰和正交的反应真正的概念。各个 FBO 本该在概念上平等,而且完全不用『 0 号 FBO 』的情况也是合理的,那么从 OpenGL 的状态机概念来讲,FBO 应该在 context 创建之后挂靠其上,而不应该和创建 context 的函数有任何语法上的直接关系。但是实际创建 context 的函数( CGLCreateContext()NSOpenGLContextwglCreateContext() )都需要一个事前创建的『 0 号 FBO 』作为参数,哪怕这个『 0 号 FBO 』完全没有用处。

终于可以泡沙发

2011/03/27

能在沙发上看电子书是件很爽的事情。但是我又不喜欢长时间用 iPad 这样不够通用的设备。宜家的这个小板子还是很不错的。喜欢开『卡车』的也可以很舒适。

并行计算的解药

2011/03/21

前几天看到 reddit.com 的 programming 类别第一名是《 Parallelism is Not Concurrency 》。读完之后发现和我去年的《多核与锁》有很多观点上的共通之处。《 Parallelism is Not Concurrency 》的开篇行文更流畅幽默,对并发( concurrency )和并行( parallelism )有更精辟的总结。比如:

Concurrency is concerned with nondeterministic composition of programs (or their components).  Parallelism is concerned with asymptotic efficiency of programs with deterministic behavior. Concurrency is all about managing the unmanageable. … Parallelism, on the other hand, is all about dependencies among the subcomputations of a deterministic computation.  The result is not in doubt, but there are many means of achieving it, some more efficient than others.

谈及『依赖( dependency )』在并行计算方面的关键地位之后,《 PINC 》进行了一个有趣的推理:

  1. 因为依赖是个关键概念而且它是高于硬件的抽象概念,所以我们需要一个基于语言的并行计算模型;
  2. 因为依赖阻碍并行,所以我们需要一个消除了依赖的并行计算模型;
  3. 由 1 和 2 ,我们需要一个能最大限度消除依赖的编程语言;
  4. 所以,functional 编程语言是解决并行计算的终极形态。

这个四步推导其实非常脆弱。首先看第 2 步,它的结论是需要一个『消除了依赖的并行计算模型』,这与《多核与锁》中谈到并行计算需要把数据分割成没有依赖关系的独立块的说法是一致的。但是,『消除依赖』是指在数据中消除依赖,是指对问题本身,或称为『问题域』消除依赖。因此准确的推导应该在第 3 步中提及的『最大限度消除依赖的编程语言』之前加上『在问题域中』这个修饰语。但是,很不幸,第 4 步的 functional 编程语言只是在编程阶段,或者称为『实现域』中消除了程序员引入依赖的能力。

人们经常想当然地把『消除』视为『消除烦恼』的同义词。但是,只有在问题域中的消除才基本保持这个同义性。而在实现域中编程语言消除程序员的某种能力更贴近于『强迫』。『强迫』的好处在于让程序员更自觉的用更好的更清晰的方法考虑问题。前提是,程序员已经拥有更好的更清晰的方法,只是经常限于时间和精力图省事抄近路采用一些丑陋的方法。这个前提在很多时候是成立的,比如说,程序员都能轻易的避免使用混乱的 goto ,而另一方面,他们又都倾向于随意使用 goto ,所以,一种禁止使用 goto 的编程语言是好的(当然,C 并没有禁用 goto ,而是把 if-elsewhilefor 等等分支功能做得易用而且优美,从而比粗暴的禁止更好发挥作用)。

很明显,这个前提在并行计算中不成立!程序员还没有一种真正通用的方法可以在问题域消除影响并行的依赖。假设一个程序员精通各种顺序排序算法,唯独没有学习过 Quick Sort 算法。那么即使学会 functional 编程语言之后,这个程序员也不可能自动的把原来的顺序算法改写成适合并行的形式。消除依赖需要在问题域的艰深研究而不是实现域编程语言的强迫。研究万有引力需要高深的微积分工具,而其结论万有引力公式只需要简单的代数知识就能在大多数场合应用。类似的,一旦在问题域中取得了消除依赖的突破,传统的编程语言往往也能很轻易的实现并行。例如并行编译这个问题,因为 C 语言在设计的时候就保证了单个文件在编译时没有对其它文件的依赖,所以并行编译只要简单的多运行几个编译器进程就能实现。(不要把这里的『设计语言』和原文四步推导中的『语言』混淆。对于编译来说,C 语言的设计是问题域而非实现域。)这时形式上消除依赖的 fucntional 编程语言倒是多余的了。

另一个例子是 3D 图形处理,这个问题包含大量的数据计算,比如顶点位置。而每个顶点的位置可以独立计算没有相互依赖。因此业界可以为这个问题建立消除依赖的硬件模型 —— GPU( 同样,这里的消除是『强迫解除能力』的意思,因为问题域本身天然地免除了依赖 )。这就给《 PINC 》中的第 1 步推导提出了反例,对天然免除了依赖的问题是可以建立基于硬件的模型的。而另一方面,因为 GPU『强迫解除』了通用 CPU 的很多能力 —— 比如要求数据的小粒度分割和缺乏对分支的支持,当业界想把 GPU 用于通用并行计算的时候,遇到的最头疼的问题是如何把原来在 CPU 上运行的程序改写成适合 GPU 的形式。当 functional 编程的拥趸苦恼 functional 语言的小众化现状的时候,其实已经有了一个流传广泛的低级 functional 编程语言,这就是 GPU 及其接口 OpenGL/OpenCL/CUDA 。这个模型已经在大多数 PC 上(包括 Mac )甚至很多 mobile device 上得到推广,实现了 functional 编程拥趸们在推广度方面的梦想。但是奇迹并没有自动出现,在问题域还是遗留了众多的工作要做。

所以,解决并行性能的关键,不在于急匆匆的在编程语言级别剥夺程序员引入依赖的能力,而在于研究更多问题的本质,去掉那些直观上存在但是并非本质上不可消除的依赖。实现域的『强迫解除』并不能带来问题的自动解决。等问题域的依赖被解除了,再考虑语言级别的问题不迟。即使到了那个时候,我也不认为 functional 语言会成为主流,因为和用来取代 gotoif-elseforwhile 相比,functional 语言解决问题的方式还是十分丑陋的。

高级动态语言与软件业

2011/03/02

顺着 Java 、Perl 、Python 一路看去会发现一个有趣的规律。至少那些 Lisp 高手会发现。后一个比前一个更 Lisp 一点。

If you look at these languages in order, Java, Perl, Python, you notice an interesting pattern. At least, you notice this pattern if you are a Lisp hacker. Each one is progressively more like Lisp.

—— 《 Revenge of the Nerds

从 Lisp 说起

好好学一门高级动态语言一直是日程上一项安排,但限于精力未着手施行。最近看了以《 Revenge of the Nerds 》为首篇的一系列讨论 Lisp 的文章,很受启发。联系我现在的工作领域,对高级动态语言的应用有些看法和期待。虽然在入门之前妄谈可以被称为『成见』,不过鉴于深入学习并非很小的投入,预先思考一下也有益处。

算法与系统

上面提到的这些文章当然都在正面评价 Lisp ,我也多少赞同。但是个人经历形成的感觉仍然是,Lisp 这样的高级动态语言(或者说以 Lisp 为终极目标不断进化的动态语言)并不会在短时间内 —— 估计十年内,成为解决软件业主要问题的手段。我不想老生长谈『性能』问题。性能的确是个问题,但我认为除此之外还有更重要的问题,在更长时间之内不会被基本解决的问题:动态语言适合编写复杂的『算法』,但不适合编写复杂的『系统』。很难给『算法』和『系统』做形式上的定义或者区分。《 Re: Revenge of the Nerds 》中引用了 Trevor Blackwell 的大致说明:

我认为,在 Web 崛起之前,绝少有程序包含比排序更复杂的算法。你认为 FreeBSD 里有复杂的算法吗?Nortel (我靠,老东家!)交换机的五千万行代码里可能连排序都没有,全是硬件资源管理和错误处理。Cisco 路由器的上百万行代码也没有几个复杂的算法。

Before the rise of the Web, I think only a very small minority of software contained complex algorithms: sorting is about as complex as it got. Can you think of a complex algorithm in FreeBSD? The 50 million lines of code in a Nortel switch probably didn’t even contain a sort. It was all about managing hardware and resources and handling failures gracefully. Cisco routers also have millions of lines of software, but not many complex algorithms.

高级动态语言适合执行简洁符号的复杂变换和推导。这类问题具有令纯脑力工作者愉悦的挑战性,属于『优雅』的问题。因此,哪怕能让这些问题的表达和解决更优雅一点点,动态语言的设计者和拥护者也会真诚地付出巨大努力。另一方面,复杂系统没有令人愉悦挑战性,包含无数细节,属于脏活累活。这是高级动态语言不擅长的或者说不屑于的。

优雅的,在智力上具有挑战性的,纯粹的符号问题可以由一个人或者一个很小的团队独立完成。为解决这类问题设计的语言更注重问题本身的表达,是高度简洁和脱离具体平台的,但是其互操作性也受到限制。

复杂的充满细节的工作则需要整个业界协作。需要芯片级别、系统集成级别、操作系统、库、和应用不同级别的互操作。这种复杂的互操作不可能通过简洁的符号来完成,取而代之的是 C 级别的 application programming interface 和 application binary interface 。曾经认为以摩尔定律带来的性能提升和业界的标准化努力能逐渐消除这类细节化的工作,但是实际趋势说明这种预计是错误的。首先是对应用的需求总是超过摩尔定律的发展,今天的 mobile 应用和 3D 应用是以前难以想像的。二是人们和计算机的交互总是不断脱离已经定义的符号范围,几十年前是命令行,几年前是菜单和按钮,今天是各种 drag and drop ,scrolling ,gesture , multi-touch ,未来将是 Kinect 和我们现在还想像不到的东西。

高级动态语言能承担的不过是整个系统中可以被简单符号定义的那部分,正如很多文档中把高级动态语言编写的算法部分叫做 kernel(不要和操作系统 kernel 混淆),只是一个小小的硬核。核内代表优美的数学推导。而外围的复杂系统才是把符号对应到真实世界的建模过程,充满了微妙而且要反复变换的权衡、妥协、近似。

通用和专用

上面所说的高级动态语言的强项限于纯符号问题只是它不能主导软件开发的一方面原因,另一方面,在纯符号问题领域高级动态语言也不是高枕无忧。低级静态语言不适合表达复杂的算法。但是这不表示它们不能。《 Revenge of the Nerds 》也承认,只要是图灵完备的语言都能等价地描述任何算法。当然该文也嘲笑了一番这种做法,它说,一般的方案是:

  1. 使用一种高级动态语言,
  2. 用(当前适用的静态语言)写一个高级动态语言的 ad-hoc 解释器(为了应付当前的问题凑合实现的不完整功能),
  3. 程序员自己充当高级动态语言的人肉解释器。

这种说法似乎成立,当程序员由于种种原因不能使用高级动态语言但是又需要实现复杂算法时,为了使设计清晰不得不自行搭建一层抽象(程序的或者人肉的)。于是,这似乎证明高级动态语言是必需的,只要这种情况是一直成立的。

但是情况会发生变化,当复杂算法足够通用以致为众多领域所需时,软件业对这个算法的投资会达到无需中间抽象而直接用静态语言表示的程度。正如快速傅立叶变换和 H.264 解码有成熟的硬件实现方式,而且不仅仅是几个具体的产品,而是一种脱离具体产品的业界普遍掌握的定式,不需要任何程序或者人肉的抽象。这些通用算法的复杂度虽然很高,却已经不需要『算法 » 高级表示 » 低级等价表示 » 硬件表示』这样的推导过程,而是完全跳过中间步骤。就像小学生每天笔算多位数乘法而不会从十进制定义的角度思考竖式计算原理。定式像生物为了快速行动而做出的不经过逻辑推导的反射行为,也许逻辑是更优雅的智慧结晶,但是反射也是进化的杰作。

我们经常说硬件的发展会减少低级语言的应用领域,但是忽视高级动态语言的应用领域也有类似的时效性。后者的阵地同样不断受到挤压,最终只剩下和各个行业的专业知识紧密相关的专业算法,比如原文中提到的航空管制系统。这些领域原本被认为是 domain-specific language 的适用范围。而动态语言进入这个原本认为应属 DSL 的领域并不奇怪,因为 Lisp 这样的语言里充满了可以把自身调教成另一种形式的 feature ,比如 macro 。按照编程的命名文化,这些创造 feature 的 feature 可以称为 meta-feature

虽然动态语言有临时构建类 DSL 语言的能力,但是这样临时搭建的 DSL 必然是千差万别没有标准的,所以用动态语言写成的代码只适合小范围的解决专业问题。。而对于需要大量互操作的问题,对问题本身的优雅和简洁的表示就会退位于对适应不同文化的交流的需求。这就必然退位于概念更为简单,更为固定的静态语言。这方面的反例是试图在通用领域引入 meta-feature 的 C++ 。把过于灵活的语言应用到整个系统,就像现代人和古人用『几何』、『骚人』这样的词语直接对话。

不完美的子语言

我并非用高级动态语言不能成为软件业的主要工具来否定它的前景。相反,我认为一切未成为大众消费级需求的符号变换操作都应该用动态语言编写。需要使用动态语言的领域会越来越多。另一方面,用高级动态语言承担超出这个范围的应用,尤其是在 I/O 和 UI 方面,虽然在产品初期规模较小的原型阶段可能很顺手,但是随着维护期的增长、代码量的扩大和团队的扩大,最后的净收益很可能为负。

我期待的高级动态语言的模式是类似 SQL 的 sub-language 模式,简化到完全省略 I/O ,嵌入到其它语言或者应用中,只关注符号操作。正如 Emacs 和 AutoCAD 提供的 Lisp 接口。可惜这种做法并没有盛行,没有什么高级动态语言真正的努力支持过 SQLite 那种以库的形式在静态语言中 in-process 解释的能力。采用高级动态语言作为算法模块的系统通常要借助 IPC 或者 Web server 来完成互操作。这也让高级动态语言不得不提供 I/O 甚至 UI 支持,成为扬短避长的累赘。而 IPC 在性能和复杂度上的开销也让很多应用用静态语言来凑合描述本该用动态语言描述的算法( ad-hoc / 人肉解释器)。

我并非 monolithic 的鼓吹者,但是我倾向于把 process separation 作为设计原则而非结构强制。比如,micro-kernel 的强制 user-space device driver 并不成功。而 monolithic kernel 允许设计者自行决定 driver 的结构成就了 FUSE 这样的 user-space driver 。高级动态语言作为一种不能独当一面的语言,最好还是把这种决定权交给应用开发者。高级动态语言并非风格一致的拒绝 in-process 形式,它们通常可以调用静态语言编写的动态链接库,但这种 in-process 交互是单向的,如上所述,动态语言的解释器很少能作为库被其它应用调用。这种单向交互是因为高级动态语言的拥护者很少把自己看作不能脱离静态语言生存的核心,相反,他们把静态语言看作是 legacy ,而且认为高级动态语言才应该是系统的 master 。

这里我们转头看看那个最初来自《人月神话》的著名论断:不论用何种语言,程序员的生产力以『行代码/天』计算的话不会有很大变化,『 bug 数/行』也不会有很大变化。是的,这个论断和我迄今为之的经历没有任何矛盾之处。在我接触过的系统里,程序员用更简洁的语言编写模块会更快,这些模块的 bug 更少,即使有,修改起来也更快。但是,如果系统出现跨模块的问题,若是涉及高级动态语言编写的模块,解决起来会比不涉及这种模块的情况难度高上几个数量级。问题不在于引入高级动态语言模块本身,而在于高级动态语言目前的实现的拙劣的互操作机制 —— 笨重的 IPC ,繁复的参数和数据结构转换。如果这一切有所改观,那么高级动态语应该可以释放更强大的生产力。

整洁之下的隐患

2011/02/23

上个月写了篇《技术的洁癖》比较了一下 Mac OS X 的单文件应用和 installer 两种发布手段,基本上只针对安装和卸载的完全度。不久之后看到某国企发布的一款应用的 installer 居然把 /etc 之下的所有文件权限改成『 777 』,想到单文件应用绝不会有这样的行为,所以写了这篇谈论一下安全问题。

安全是一个复杂的多层面的话题,在讨论普通 Unix 和 Windows 这样的基于 access control list 的 DAC 安全模型时,往往仅仅限于满足一个基本的要求 —— 恶意代码不能修改或者毁坏当前用户之外其他用户的和系统的数据,但是并不要求恶意代码无法修改或者毁坏当前用户自己的数据。在某些高标准的环境中,这个基本要求是远远不够的。但是对于大多数个人应用来说,只要满足这个基本要求,就可以通过系统本身的合理配置建立『沙箱』隔离恶意代码。

这个标准同样可以用来要求应用程序的发布方案:安装和卸载应用程序的过程不能修改或者毁坏当前用户之外其他用户的和系统的数据,但是并不要求恶意代码不修改或者毁坏当前用户本身的数据。

要分析这个要求是否能得到满足,先分解一下安装过程。一般来说,安装应用包括两个动作:

  1. 把应用程序的可执行文件拷贝到目标机器的硬盘(或其它存储设备)。这个过程可能包括文件的解压缩等操作 [1]。
  2. 执行一个或者多个『脚本』[2] 来创建或者修改一些配置文件。

简单考虑,如果保证每个用户拥有各自独立的可执行文件拷贝和配置文件,那么只要让安装的整个过程都只在普通用户权限下执行,前面所说的对安全的基本要求即可得到满足。

对今天的系统和大多数应用来说,保证每个用户有各自独立的配置文件不成问题,但是大多数系统都要求应用的可执行文件由所有用户共享。一般情况下,应用的可执行文件都要拷贝到系统文件夹之下。这意味着如果使用 installer ,就必须用系统管理员权限来执行。这就给了 installer 绕过『不能修改或者毁坏当前用户之外的其他用户和系统的数据』这个要求的机会。

有人会考虑把 installer 分解成两部分,分别执行上面的第一步和第二步,而只给第一部分系统管理员权限。可是,对于一个可执行的 installer ,即使名义上分成了两部分,用户也很难限制第一部分做什么,它完全可能毁坏系统数据,或者更糟,把安装的可执行文件的 ACL 设置成可提升权限 [3] ,又或者把所有系统文件的权限改写成『 777 』。这是一个看似可行但是错误的方案,因为最小权限分离仅仅适用于防止可信程序由于 bug 被恶意代码劫持权限,而 installer 在这个情景中是『可疑』的,其行为是用户不能确定的。

真正可行的办法是让用户调用操作系统的命令而非 installer 来执行第一步 [4],因为系统命令本身是可信的(trusted),即使用管理员权限运行系统命令,只要正确输入参数,其行为是用户可以确定的,不会造成危害。

上面这个过程其实就是设计应用发布方案过程中的一个需求迭代,当安全因素纳入考虑之后,迭代的结果把最初的基于 installer 的方案变成了一个类似 Mac OS X 单文件应用的方案:

  1. 单文件应用的第一步完全依靠 OS X 本身的 Finder 完成。虽然需要管理员权限,但是应用本身,即便是恶意的,也没有机会在这个过程中采取恶意行动。
  2. 单文件应用的配置文件在应用的第一次运行时生成,只要始终用普通用户运行,恶意程序没有机会毁坏其它用户和系统数据。

从这个对比可以看到,installer 确实给了有技术洁癖的人一个整洁的装饰板,可是饰板之下未必就是优雅。以高度互信作为前提的『整洁』的方案,到了『可疑』环境中可能悄悄的毁坏数据。如果非要坚持其表面的整洁,整个方案也很难通过简单的改动弥补安全方面的问题。

当然,在满足本文涉及的安全需求方面,只有单文件应用拥有优势。而 OS X 本身就存在其它安装方式,而且也在不断引入新的安装方式,比如 Photoshop 的定制 installer 和 Mac App Store。不过本文主要针对的是在不考虑 kernel 的严重 bug 的情况下,限制『可疑』应用危害的方法。而 Photoshop 和 Mac App Store 之类的方案则从其它方面提高了应用的『可信度』。那是另外一个话题了。

注释:

  1. Mac OS X 的单文件应用不包括这种操作。
  2. 这里的脚本并非狭义的纯文本解释执行的脚本文件,而是值这种配置操作实质只是一种脚本类的操作。
  3. 比如 Unix 文件系统的 s 属性,可以让一个可执行文件即使被非管理员用户执行也拥有管理员权限。
  4. 对于采用特殊的私有格式进行打包或者压缩的应用,这种操作很困难。

OpenGL 随想(四):计算机如何解决问题

2011/02/05

之前《OpenGL 随想》已经写了三篇()。题目中虽有 OpenGL,但内容并非介绍 OpenGL 或者讨论其技术细节,而是讨论学习过程中联想到的一些计算、软件方面的问题。OpenGL 主要作为引发思考的灵感和一些例子的来源。

这些联系很松散的文章被归于一个系列为的是体现研究 OpenGL 这样一个复杂系统的过程中能引发各种思考。在计算软件领域之外也有类似情况:研究第二次世界大战的历史可以引发人际关系、项目管理、市场走向等等方面的思考。这个系列算是对这些辉煌的灵感源泉一个微不足道的致敬。

聪明和笨

前几个月,有人总结:计算机系统的性能提升中,算法改进导致的部分超过了遵循摩尔定律的硬件发展所带来的部分,前者达到后者的四十多倍。考虑到数学等基础领域出现突破进展是多么困难,而另一方面硬件一直沿着摩尔定律设定的路线坚定不移地提升(指数发展,每 18 个月每单位晶体管数量增加一倍),这个数字有点让人惊讶。

换个角度想,也就是说计算性能的提高的很大一部分不是归功于电子学和材料学的进展,而是归功于人们原本采用了比较『笨』的数学方法。而且提高的幅度之大说明一开始的方法『笨』的不轻。从纯数字看似乎的确如此,但从另一个角度看这是个错觉。

OpenGL 的策略

OpenGL 和计算机三维图形领域的一些方法所体现的『聪明』可以很好的解释『笨』的含义。比如考虑一个基本的问题,在计算机图形中如何表示一个三维平滑曲面,比如一个球体?最直接的方法是把曲面的表面离散化,通过数学方法(比如球的解析式或者 NURBS 表示)求出各个离散点的坐标,然后用离散点构成平面。离散点分布越密集(术语叫采样频率高),每个平面越小,平面越多,结果就和真实的曲面越吻合。当采样频率在 viewport [1] 上的投影超过屏幕的分辨率的时候,就会和平滑曲面无异 [2]。遗憾的是,估计当今主流台式机硬件用这个方法连五年前的高端游戏效果都得不到。OpenGL 采用另一种方法:smooth-shading 。这种方法采用的采样频率不是很高,只计算每个离散点当前视角和光照条件下在 viewport 上的位置和色彩。然后在二维的 viewport 上对各个离散点的投影之间的区域进行二维色彩线性插值。也就是说,OpenGL 的曲面是在 viewport 上用二维位图处理,或者可以叫『伪 3D 』方法实现的。

正是这种『伪 3D 』方法,让个人计算机在七八年以前就实现了高度仿真的实时三维图形效果。那么是不是可以说这是一个『聪明』的方法而提高采样频率是个『笨』办法呢?并非如此。如果希望计算机图形系统能自动完成任何光照条件下对各种材质、包括反射等等效果的自动处理,特别是当仅仅知道光照条件和材料特征的情况下希望通过计算的方式得出场景呈现的效果,这是唯一的方法。OpenGL 采用的『伪 3D 』方法不能直接推演真实的效果,它所做的只是用一些近似方法得到任意的效果,而后必须由人来主观判断这些效果是否足够和真实场景接近。除平滑曲面之外,OpenGL 中通过 viewport『伪 3D 』方法来完成的效果还包括物体遮盖、半透明、反射等等。

另一个例子是阴影。如果要自动生成阴影,图形系统必需跟踪场景中物体对光源遮挡的效果。由于性能的原因,OpenGL 完全不做这种光线追踪处理,阴影效果要求程序员通过投射变换计算出位置和形状之后手工绘制的黑色或者灰色形状(只有透射变换计算本身可以通过 OpenGL 提供的函数完成)。而且,简单的投影计算只能呈现理想点光源的全影,无法实现太阳这样的面积光源形成的半影。如果要形成半影,程序员还必须自己写代码通过二维位图处理(也可以看作『伪 3D 』)算法生成阴影然后画在投影面上。同样通过手工计算完成的效果还有镜面反射等等。

OpenGL 并不是能直接从物体描述自动生成场景的虚拟照相机,大量二维位图处理的应用使它更像画具,只不过增加了一些稍稍方便和精确的三维旋转和透视的处理。用 OpenGL 直接构建应用程序的过程更接近绘画而非场景布置或者摄影。[3] 虽然从历史角度说 OpenGL 的设计不见得就真的经历了从『面向场景和摄制』到『面向绘制』的转变,但是很多惊叹 OpenGL 效果而不明就里的人头脑中确实把 OpenGL 的功能想像成前者。所以从认识的角度以及类比到更广阔的领域,OpenGL 做出的主要『优化』本质上是改变了需求或者说问题域。

这是计算机提高解决问题效率的一个主要方式:回避问题,改变问题!虽然目标仍然是为用户呈现高度真实感的虚拟场景,但是 OpenGL 几乎完全剔除了一切『虚拟现实』的能力。它采用与真实场景和摄影相比高度简化的近似方法,要求系统的构建者通过实际经历、想象力、或者其它 OpenGL 之外 [4] 的方法另行得出,甚至是事先得出真实场景应该呈现的效果,通过主观比较判断 OpenGL 的近似效果是否令人满意再对不够满意的地方做出调整。和表面的感觉不同,OpenGL 把看似精确的计算机图形变成了一个主观创意的问题。

问题域和人的价值

计算机领域的很多所谓『算法优化』其实是『问题域』的简化或者说窄化。一开始面对一个过于复杂的问题域,采用性能无法被接受的算法。而后慢慢收回过大的野心,用更粗糙的算法来满足更简单的问题。

对问题域的修改即使并非唯一方法,也是从硬件之外提升计算生产力的主要方式。比如代码优化这个问题,C++ 就开始放弃了对语义和程序员透明的原则,引入了需要程序员干预的 ROV 优化。相比之下『空间换时间』等等经典的优化理论受到硬件的制约比较大,可以视为硬件发展的附庸,比如廉价的内存才能保证大 cache 算法的可行。而且硬件的改进会让一些优化技术失效,比如近年 CPU 片内 cache 和主内存之间速度差距的迅速增大已经让编译器的『体积最大』优化策略难以换取『速度最快』的结果。

对原问题域的简化并非意味着对被简化部分的简单忽略,很多的简化是把问题中计算机可解部分和人类干预部分的合理划分 [5]。OpenGL 这样的计算机图形系统的功能必须通过各种建模工具的交互式界面和额外材料来填补其『面向绘制』问题域与普通人能胜任的『面向场景和摄制』问题域的差距,才能在人类的辅助下释放最大潜力。严格限制计算机可解部分而得到更高的计算效率,通过良好的UI 把其它交给人类解决。这也是 UI 设计的意义,精致 UI 的作用不光是让人类用户享有更好的用户体验,也是为了释放计算机自动处理部分的最大潜力。

注释:

  1. Viewport 可以理解为 OpenGL 中虚拟摄像机的底片在操作系统的窗口上的实际呈现。
  2. 这里的『无异』是在硬件呈现能力的范围内而言。不过今天的硬件也已经进入了 ratinal screen 的时代,可以说在人的感知能力范围内也可以做到『无异』了。
  3. 很多三维设计师的自我感觉更像摄影师或者场景布置师,这正是 3DS Max 或者 AutoCAD 等软件在 OpenGL 之上提供的附加值。
  4. 这种方法不一定是脱离计算机辅助的。比如上面提到的 AutoCAD 和 3DS Max 等软件的帮助。但是它们是 OpenGL 之外的。而且是通过人类主导的。
  5. 某些创造性工作或者人脑结构更适合解决的问题还能让人类保持一种优越感和尊严。