Prime Ridant

2011/01/26

Asimov《基地》系列的主线『心灵史学』(psychohistory)是不可能的科学。Asimov 年轻时受热力学启发构思了『心灵史学』。他自己后来坦承,热力学的简单分子运动和人的行为毕竟有天壤之别,如果稍稍年长一些可能就没有勇气仅凭这么牵强的启发构想如此宏大的科学。

步入老年之后,Asimov 开始创作《基地》三部曲的前传。第一部《 Prelude to Foundation 》描写 Seldon 还是一个从边远星系来到银河帝国首都的年轻数学家,虽然提出了『心灵史学』的概念但是自己都不相信它能成为预言的实用科学。守护人类的机器人 R.Daneel 用了几个月帮助他建立了把这门科学推向实用的决心,而那之后十多年的时间里尽管有 R.Daneel 作为帝国首相的全力协助,『心灵史学』的实用化进展甚微。

前传第二部《 Forward the Foundation 》中的一个转折点之后,『心灵史学』的实用化开始稳步前进。这个转折点是 Prime Ridant 的发明。通过 Prime Ridant ,所有参与『心灵史学』研究的数学家能够随时地持续地对其全部公式进行修改和审查。『心灵史学』的实用化程度逐步达到可以被称为『 Seldon 工程』的地步。

我当初读到这段的时候觉得 Asimov 描述的情节太过唐突。难道在 Seldon 困惑了十几年之后令『心灵史学』的研究一下子步入正轨的只是一个审查和存储工具?我记得,当时我把这段重新读了一遍,然后放下小说。过了几天,又读了一遍。这次我看到 Asimov 描写『心灵史学』的公式体系远远比现实中的数学复杂,突然豁然开朗 —— 当面对的复杂度超过某个程度时,高效稳定的持续集成和审查工具确实能起到让工作发生质变的作用。从复杂度来说,Asimov 笔下的『心灵史学』已经不太像数学,而是更类似今天的软件代码。所以一切明朗了:Prime Ridant 就是一个 version control 系统,拥有海量的存储,稳定的并发访问能力,方便的审查功能,以及优秀的用户界面(三维投射)。从小说的一些描写还能看出,Prime Ridant 还提供不错的 branching 功能,因为还未经过严格审查的修改只能进入相对不稳定的分支。

我不知道 Asimov 是否了解软件开发。即使他对软件开发的任何知识都闻所未闻,我也不会觉得把 Prime Ridant 和 version control 联系到一起有丝毫的牵强附会。在《 What Techology Wants 》一书中,《连线》的创始人已经说明了人类社会的技术和思想在并行独立发展中的自发汇聚现象:三个学者同时接近完成进化论(虽然达尔文第一个发表),20 个人同时独立接近发明电灯,非欧几何被至少三个数学家同时独立构建,三个物理学家同时接近完成相对论,等等。Asimov 作为一个对技术有独到见解的名家,在设想人类如何构建高度复杂系统的时候,描写的虚构工具和 version control 系统有众多相似之处,这种思想的汇聚本身就说明了 version control 在软件开发中的本质核心地位。

OpenGL 随想(三):同步与多线程

2011/01/21

去年十月写过一篇《并行执行》,讨论了多核与锁的问题。提到了『并行执行』和『并发访问』两个概念。最近看公司里面老外写的文档,也用了同样的概念,不同的名称,分别叫做『性能化多线程』和『功能化多线程』。根据公司里项目的实践,『并行执行』除了像《并行执行》里讨论的向 GPGPU 转化之外,也在向 Intel TBBMKL 之类的 non-lock-based 技术转化。锁这个技术,在『性能化多线程』中的应用已经逐渐萎缩,并且我认为『功能化多线程』也会为了降低复杂度而逐渐向 timer 等 cooperative 并发技术转移。

本质上说,OpenGL 也是『性能化多线程』。这种多线程的特点是所有线程都不共享数据,所以无需中间同步。只需要最后的结果同步一次。类似主线程对所有性能化线程调用 join 。OpenGL 有两个操作可以提供这种 join 的语义,glReadPixels()glFlush() 。这两个操作都是为了防止用户看到数据(渲染结果)的不合法状态。比如 glFlush() 经常作为 glutSwapBuffers() 的隐含步骤执行。

来自代码

2011/01/18

舍弃旧代码是程序员经常面对的一种诱惑。不必说维护旧系统的人经常要面对,就是开发新功能新产品的人也时常如此 —— 因为你经常被建议要借鉴一个老产品的 code base ,而你本能地想拒绝而重新打造一个干净的系统。幸运的是,很早以前我已经开始相信舍弃这些东西是不对的,不应该重写任何不必重写的东西。当几次看到那些险些被舍弃的旧东西节省了大量工作之后,我更是坚信对那种看起来又老又旧的东西,应该留出一个星期的时间来仔细研究它是否有用。这一个星期可能会节省一个月的工作。

这个原则后来读到的很多文章里得到印证。但是我记得开始编程的时候完全不是这么想的。最近一个星期我仔细回顾了一下这个转变过程。

2005 年开始看 Linux kernel 的代码。我当时有一种稍稍自卑的感觉,感慨 Linux kernel 的代码为什么能把一切情况都考虑周全。比如一个 40 行左右的函数,让我来写最多只能写出十几行,而差的那 30 行处理的是很重要又不容易考虑到的情况。

当时看代码是用《 Understand the Linux Kernel, 3rd Edition 》做主线。书上讲的是 2.6.x 版本( x 是一个远小于 11 的数字)。而我参照的源代码是从网上下载的最新稳定版。从 2.6.1x 一直到 2.6.2x 更新了很多次。其间越来越多的看到书上的代码和最新代码的差异。跟随这些差异我开始阅读不同版本的 release notes ,以及针对特定修改的 mail list 讨论和 patch 注释。最后我意识到,书上的版本和最新版本之间的差异,往往就类似于上面提到的我能写出的十几行和实际的 40 多行的差异。为什么能把一切情况都考虑到?答案就是持之不懈地 Fix Bug 。

回想起来,尽管不够清晰,但这应该就是『绝不重写无必要重写的代码』这个想法产生的过程。没有人能把事情一次做对。旧的代码有很多错误,但是抛弃它们只是让已经犯过的错误有机会被重犯一次,加上新的错误。

另一方面,那些最后形成的代码,如果不比对《 Understanding the Linux Kernel, 3rd Edition 》上的旧版代码,似乎像是一次写就的。这是我最初的自卑的来源,也是很多初学者对编写代码的错觉。人们其实并不像最后结果体现的那样能一次就把事情做对,但是我们对错误的修正也没有必要直接体现错误本身。在循序渐进的过程中要把结果造就成一次就做对的面貌。如果你真的想回忆每个与之战斗过的错误,version control 才是怀念历史的地方。

已经有三年多没有碰过 Linux kernel 代码了。回顾起来,阅读它的代码让很多正确的想法第一次被植入到我头脑中。对于程序员来说,更多正确的有用的东西还是来自代码。

技术的洁癖

2011/01/11

看到 Twitter 上讨论 Mac 的单文件应用的卸载问题。首先说明一下,单文件应用是 Mac 上一种很常见的发布方式,整个应用只有一个文件(其实是一个目录),安装即拷贝该文件到 /Applications 下,卸载即删除这个文件。Mac 下还有很多种其它发布方式,比如 Xcode 使用的是 Apple 推荐的一种多文件 installer ,Photoshop 是 Adobe 自己设计的 installer ,而且 Mac OS X 也不会限制任何人发明定义自己的安装方式。这里只讨论单文件应用。

精确的说,单文件应用的安装实际发生在第一次启动,这时应用会按照需要在用户的数据目录下创建必要的配置文件。所以,简单的删除应用文件这种卸载方式其实确实会留下一些垃圾。

这个问题给了两种人不同的反应。第一种人是一定要把 OS X 的单文件卸载和 Windows 下流行的反安装(uninstall)比较的人。我想分两个层面讨论这种反应。首先,Windows 的反安装鼓励软件发行者安装的时候做了什么卸载的时候就反过来做什么;而 Mac 的单文件应用卸载鼓励发行者置少量垃圾于不顾。从理想层面说 Windows 是占优的。可是实际上,没有一个 Windows 应用的反安装能完全兑现理想,甚至很多应用连自己的 DLL —— 也就是可执行文件都删不掉。Windows 和 Mac 的方案相比较,前者是一个完全兑现就能美好的不得了,但是稍有差池就跌下地狱的方案,后者是一个基本能 100% 兑现但是天然有轻微缺陷的方案。

第二个层面是要追寻卸载应用的动机。自从买过上一台 MacBook 到现在用这台 MacBook Pro ,我从来没有卸载过一个应用。原因很简单,硬盘够用,系统也没有变慢。用 Windows 的时候我也几乎不用反安装功能,因为卸也卸不干净;而且一般三个月左右,最多四个半月系统就慢到必须重装的地步了。

被这个问题引发兴趣的第二种人是宣称自己喜爱整洁的人。对他们来说,就算系统不变慢,硬盘还有 2T 空间,只要想到还有几个 k 的垃圾文件隐藏在边边角角就浑身不得劲。对于这种把『喜好』、『就是』作为原因的人,仔细从人性和人类发展方面思考一番总是能得到些乐趣。比如说,装修摸墙面的时候都得把墙面刮花。如果一个人坐在整洁的客厅,想到墙面下其实是刮得乱七八糟的墙体而百爪挠心呢,是不是比较分裂?还有今天的文件系统早就不是那个一周用 defrag 整理几次的 FAT16 系统了,它会按照自己的工作把文件随意摆放。喜好整洁的人是否也不乐意呢?Joel 曾经看到过一台『肮脏至极』的面包机,而事后证明那是制造美食的好工具。其实在维系人类正常发展的效率需求之前,洁癖是没有生存空间的。

最后声明一下,我不认为 Mac 的方案是完美的。更进一步,不论是 Windows 的安装/反安装加注册表,Mac 的单文件应用加不完全卸载,还是 Linux 的集中化包管理,都有很大的改进空间。只是所谓基于『卸的干净』的洁癖这种思考方式太过简单了。

分裂的代价

2011/01/01

当 Microsoft 刚刚推出 Direct3D 的时候,PC 还是一个弱小的平台,接受 OpenGL 这样的工作站级别标准很遥远。所以『单独搞一套』似乎是天经地义的。当 NT 平台和 Windows 95/98 逐渐合并的时候,工作站级别的 OpenGL 和 Direct3D 功能重叠的问题开始被关注。最后,Microsoft 退出 OpenGL ARB 说明了它的决定 —— 用私有标准来 lock-in 开发者的 code base 和 knowledge base 。

经过了近十年之后,Direct3D 已经成为 3D 领域中一个功能上受人尊敬的竞争者,但是其分裂初衷实现得如何呢?去年年初,游戏大牛 David Rosen 写了 《 Why You Should Use OpenGL and not DirectX 》,说明在游戏开发界有众多严肃的开发者坚持开发基于 OpenGL 的产品,其中部分甚至是只在 Windows 上开发也不涉及 OpenGL 之外的接口。而在 Rosen 文章之下的评论里不乏反对的声音,其中一个理由是:只要软件的结构清晰,从 Direct3D 转向 OpenGL 代价并不高,可以说除了资源的确捉襟见肘的 just startup 都能应付。两种声音说明,无论是对于有强烈主观倾向的开发者,还是对于重视实用可操作的开发者,lock-in 都并不成功。

对 Microsoft 来说,进一步展示抛弃 OpenGL 的决心,也许会让开发者三思。所以出现了 Vista 剔除 OpenGL 的言论。但是事实是,Microsoft 不能在大量的现有 code base 的压力下冒天下之大不韪。它仍然必须提供与 Direct3D 同样完美的 OpenGL 支持。

Dirtect3D 让 Microsoft 拥有一个完全可控的标准。但是这种可控度并非在 OpenGL 上不可达到。OpenGL 和 Java,HTML 这类标准不同。作为一个对功能覆盖十分克制的专业标准,OpenGL 天然的必须和平台相关的代码混用。作为 native code 的源代码级别标准,OpenGL 天然的必须重编译才能跨平台。所以 OpenGL 的开发者对于私有扩展和细节差异的容忍度要远远高于 Java/HTML 之类开发者。为了平台可控性而创立 Direct3D 的必要性是值得质疑的(当然平台可控并非 Microsoft 的全部目的)。

应该说 Direct3D —— 尤其是 DirectX 11 的版本 —— 是一个优秀的开发平台。但是对微软来说,现状是这样的:无法抛弃 OpenGL,所以必须维护两套并行的接口,加大了维护成本。对于可控性的收益来说,投入大大高于必要。Lock-in 开发者 code base 的目标基本失败。对于平台的创立者来说,这是一个教训:应该对 JVM 这种基于 VM 的跨平台方案保持距离,但是对 OpenGL 和 POSIX 这样宽松的源代码级接口进行分裂是毫无必要的。(附带说一句,Windows 的 POSIX 子系统面临类似的程度稍轻的窘境。)

达斯勋爵

2010/12/27

周末逛街。

从量子计算到 OpenQL 的一知半解

2010/12/25

量子计算可能是计算领域的下一件大事情。每次关于量子计算的新闻都能引起程序员们对于世界是否会被颠覆的讨论。幸好上帝似乎把这个门槛弄得很高,世界才能继续按照今天的规则运转。既然可能死在这上面,最好搞清除是怎么回事。可是我对量子力学的理解力也就限于《原子中的幽灵》那种科普水平了。加上五年里断断续续看到的文章,此文的一多半纯属猜测。

五年前我对量子计算模模糊糊的理解是利用量子的状态叠加特性,实现天然的并行计算,以至于能在多项式时间计算某些 NP 问题。但是,既然量子状态叠加会在被观察的一瞬间塌陷,所谓天然的并行计算带来的绝对好处也只能存在于结果产生之前。在从输入到结果的整个宏观态过程中,必然有一些缺陷来抵消天然并行计算的绝对好处 —— 这个缺陷是什么,这是五年来我对量子计算的疑惑。最近几个月根据新看的资料慢慢猜测,这个缺陷可能就是量子计算的结果并不能保证每次都正确,正确的概率是一个比较接近于一的数值。至于计算结果的不确定性是怎么和状态塌陷过程联系起来的就超出了我的理解能力。但是一般来说,用一种缺陷换取一种好处,已经是普通人能理解的宇宙准则了。基本上大多数人考虑一件事情如何影响自己的生活也只需要这种程度。

虽然量子计算的结果是不确定的,但庆幸的是验证一个给定的结果是否正确一般非常容易。比如,解一个二次方程,和验证一个数是不是一个二次方程的解,显然后者更容易。(但是也有极少数人相信两者一样容易。大多数人相信后者更容易,但迄今无人能证明。这就是著名的 P != NP 问题。)所以重复运行几次量子计算求解加经典计算的验证就能正确解决某些 NP 问题,虽然过程要重复几次并且需要额外的验证步骤,但是这些低阶的开销不会妨碍计算过程的时间复杂度仍然保持在多项式级别。

因此基本上,量子计算类似 GPGPU 计算这样的机制,能解决特定问题,但是不能解决通用计算的所有问题。就像编译器不可能用 GPU 加速,大概应用程序 UI 的代码也不会量子化。量子计算必须依靠经典计算的辅助(就像 GPGPU 需要依靠 CPU 的辅助)。等到量子加速芯片放到 MacBook Pro 里的那一天,世界不会被颠覆,只是系统中会多一套叫做 OpenQL (Q 是 quantum 的 Q)的开发框架。

C 是 MVC 的 C

2010/12/15

开发桌面应用的时候,model-view-controller 是技术讨论中最为频繁引用的概念之一。所以一直想写篇关于 MVC 的。不过总是犹豫如何入手谈论这个复杂的话题。上周记录了一些学习 OpenGL 的想法,其中一篇和 MVC 有关,有人提出,MVC 是不是 over-engineering ?特别是 controller 这个概念是不是多余的?

质疑一:

没有人觉得这是 over engineering 么? 但凡任何一个把界面代码和业务逻辑代码进行一定程度的分离的程序不都有 view 和 model 么?至于二者之间怎么耦合方法可以多种多样,有必要非得显式设计成什么 controller ,而且还得按照 diagram 上的方式一本正经的互动?

我的意思是说,且不说设计模式是好是坏尚无定论,本来 GUI 编程里需要考虑的方方面面就多,如果 system GUI APIs 还非要强迫你去按照什么设计模式来把 M、V、C 的子类 plug in 才能用就未免太死板了(让我想到了当年失败的 EJB)。

质疑二:

我一直觉得 controller 是个多余的设计。

逻辑层提供 API 给显示层调用,提供(支持 multi-cast 的)callback 机制(callback function、event、或是 ugly 一点的 listener/observer)通知显示层。这样,对于显示层来说,主动被动(或称从上往下、从下往上)的情景都涵盖了,不就够了么,要单独搞个 controller 做甚?

就你说的例子,多个 view 同步可以用注册逻辑层的 multi-cast 的 callback 的确保一致性;异步本身可以封装在显示层内部(也就是逻辑层的 API 是同步的,显示层自己实现异步的假象),不关 model 什么事;drag-n-drop 更是显示层局部的代码互调,也不关 model 什么事。

首先,『如果 system GUI APIs 还非要强迫你去按照什么设计模式来把 M、V、C 的子类 plug in 才能用就未免太死板了』这个观点我认为是无的放矢了。我从来没有看到任何一个 framework 会强迫你按照 MVC 来实现一个程序。不管是 Qt 、Cocoa 、MFC 、Win32 SDK ,你总是可以用很简陋的方式去实现你的 UI 。或者如果头脑够健壮,你完全可以用意大利面条的方式实现复杂的 UI 。『EJB』倒的确是一个经典的强迫开发者实现一大堆 over-engineering 接口的例子,只是除了 Sun 当年会做这样的蠢事之外,这种灾难几乎没有被重复过(即便是同门的 AWT/Swing 也没有像 EJB 强迫接口实现那样强迫 MVC)。

至于说『异步本身可以封装在显示层内部』这种说法,我认为主要基于想象而不是实际的工程经验。实际中,异步操作不可能封装在显示层内部,更不要说很多异步操作还要求显示中间状态,这些中间状态的合法性要靠 model 来判断,否则就不是中间状态而是花屏了。

本文主要回答另一个问题:什么是 controller ,controller 是否必要。我初次接触 UI 开发使用的是 MFC 。MFC 的 UI 开发模式官方称为文档-视图(view-document)—— 没有提到 controller 。像 MFC 这类略过 controller 不谈的 framework 还有一些。

什么是 controller ,实体和规则

从下图表明的定义来看,controller 有三个特点:

  1. 把 UI framework 的底层消息翻译成修改 model 的高层命令;
  2. 设置 view 的状态(比如 active/inactive ,谁是 main view),注意 controller 修改的是 view 的一些简单状态,不能触及 view 的主要显示。其实这是 controller 最弱的一个可有可无的功能。
  3. Controller 只能修改 model ,不能直接修改 view 。View 的修改必须由 model 的通知触发,由 view 本身完成。

上面的第一点可以用来划分在一个 UI 系统中什么是 controller :一般来说,那些直接处理底层消息的回调函数就是 controller 。比如,MFC 里 CViewOnCommand() 。Cocoa 里 NSResponder 的 mouseMove() 。Controller 不需要非得是单独的一个或者几个类。那些认为 controller 是『多余』的质疑往往针对是否需要一个或者几个 controller 类,这是对 MVC 的误解。但是,虽然 controller 无需作为单独的类来实现,它的代码与 model 和 view 的划分还是有明确界限,而且 controller 代码的编写必须符合上面描述的第二点和第三点。

要特别说明的一点是,虽然 controller 不需要非得是单独的类,但是在复杂的应用中单独的 controller 类会带来好处。当 view 的不同实例需要对用户的相同操作做出不同的反应的时候(比如显示缩略图和显示稍大preview 的 view 可以是共用同样的 render 代码,但是会提供不同的操作),单独的 controller 类可以让代码更清晰。

所以,controller 只是告诉开发者如何把消息处理函数实现得更好,更容易维护的一个概念。把它理解成强迫你实现一个模块的 over-engineering 有点强迫妄想的。

Controller 不能做什么

一个通常的对 MVC 的误解是 controller 用来控制 model 和 view 。应该采用的规则是:

  1. Controller 修改 model ;
  2. Model 通知 view 发生了改动,但不能通知改动了什么;
  3. View 自己负责更新可视化效果。

第一个问题是 controller 为什么不能修改 view 。答案是 controller 和 view 都不应该实现 submit change 的逻辑。Controller 的职责是把用户的操作(可以看作对 change 的低级描述)翻译成 model 可以接受的高级 change 描述,但是它不能干涉如何把 change submit 到已有的数据中。View 的责任是如实的表现 model 当前状态(确切的说是最近的合法状态),它也绝不应该涉足把一份增量数据合并到已有的数据集中这样的任务。这样的合并可以很简单,但是也经常很复杂,尤其是对于那些一致性约束很复杂的数据。一个应用应该只有一份代码负责这样复杂的逻辑,那就是 model 。

第二个问题是为什么应该由 model 而不是 controller 通知 view 发生了改变。答案是因为只有 model 知道何时应该通知。如果 model 提供的每个操作都是能原子化的保证数据一致的和同步的,那么 controller 调用这样的操作之后立即通知 view 也未尝不可。但是复杂的应用中,model 的操作往往是异步的、后台进行的、和并发的。如上面提到的,这种异步、后台和并发的特性不但是因为各种客观条件,而且有时是主动设计的。比如耗时很长的操作,又需要展示中间状态,就不能做成一个同步的操作。只有 model 能知道什么是最恰当的时间通知 view 进行更新。

暂态数据能不能由 view 管理

如果一份数据仅仅用于显示临时的可视化效果,能否让 view 直接管理?答案是具体情况具体分析。取决于如何定义暂态数据。比如,有些数据仅仅用于一个操作期间的显示效果,比如拖拽,或者长时间的 cache 更新。问题在于这样的长时间操作期间,UI 并不阻止用户发起其它操作,这些新发起的操作可能和原有操作交替或者同时运行,可能新操作的结果部分或者全部受到原有操作结果的影响,或者新操作可能会终止或者暂停原有操作。而这些复杂操作中可能就包括文档的打开或者关闭操作。

所以,暂态数据不是绝对不能由 view 管理,但是不要轻视 model 的作用。很多时候,让你的 controller 恪守第三条准则『Controller 只能修改 model ,不能直接修改 view 。View 的修改必须由 model 的通知触发,由 view 本身完成』,会让代码的整体复杂度大大降低,也能减少今后必须增加 UI 功能时所面临的引入新 bug 的风险。

什么是 controller

Model 和 view 之间的信息流动必须是有序的。如果假设没有 controller ,那么上面那个图里的五个箭头就必须都画在 model 和 view 之间。如果两个实体之间的箭头有五个之多,这些箭头的端点实际附着的地方肯定是不同的。这些附着点就在 model 和 view 就会形成不同的子模快。所以,除非能证明把这五个箭头中的两到三个是多余的,否则你还是不得不从 model 和 view 里重构出一个实体。只有让实体之间的箭头控制在一到两个,一个 pattern 才真正具有降低整体复杂度的意义。这也正是质疑一里所谓『二者之间怎么耦合方法可以多种多样』的问题所在,对于复杂的 UI 系统,『多种多样』并不是实际可以采取的方法,只是没有实际经验的臆想。

上面对 model 和 view 的讨论说明,这五个箭头缺一不可。所以,model 和 view 模块里实际包含着一些子模块来承担不同箭头的附着点。这些子模块的概念就是 controller 。

Controller 不是一个束缚代码物理位置的约定。它是一个设计的概念,帮助开发者理清信息流动的方向。它是一个刻度,让开发者明白 code base 的行为。你可以说,我的 code base 中 controller 很薄(或者很厚)而且我明白原因。但是如果你说:哦,那是 over-engineering ,我一直都把 controller 这个概念从头脑中清除出去,那你实际上不知道自己在做什么。

OpenGL 随想(二):声明式与 MVC

2010/12/10

上篇的结尾提到了声明式 DSL 和 OpenGL 的汇编风格的差别和联系。其实我当时还联想到 model-view-controller 模式,这次另起一篇。

能有这样的联想是因为引入 OpenGL 的产品几乎都是 MVC 架构,而且代码中 OpenGL 相关的部分几乎完全被限制在 view 模块中。具体点说,在基于 Cocoa 的代码中,OpenGL 的使用几乎完全被限制在 NSView 子类的 drawRect: 消息中。在 Win32 程序中几乎完全被限制在 WM_PAINT 的处理函数中。也就是说,复杂的 MVC 架构的应用是用汇编风格来写 view 。另一方面,可以把 model 看成是 view 的声明式描述。

所以可以这样来解释 MVC 模式:每个 model 模块的定义都是一个针对当前应用定制的声明式 DSL ,每个 view 模块都是这个 DSL 的(可视化)解释器。OpenGL 的风格正好符合高效解释器通常需要汇编语言来编写这个惯例。在一个设计良好的 MVC 应用中,不仅是模块功能的分离,而且隐含了两种编程范式(paradigm)—— 声明式和命令式。

如果有一天真的出现了声明式的 3D 引擎又会如何呢?比如 Java 3D 当年就尝试过类似声明式的概念。我想也许到那时 view 模块会成为 MVC 模式中对底层引擎很薄的一层封装(而今天 MVC 中 controller 是最简单的一层)。又或许我们的应用永远需要一个比通用的声明式语言更定制化的 DSL ,而 view 就成为这两种声明式 DSL 的翻译器。

OpenGL 随想(一):C 格式机器码

2010/12/09

自从 2 月份研究了投射矩阵(projection matrix)之后,学习 OpenGL 的事情就放下了好一阵子。直到最近工作上有了实际的需求才重新拾起来。

附带说一下,《OpenGL SuperBible》这本书真的不错,虽然已经多少年对名字含有『Bible』的书敬而远之了。不过纠结了很久是买第四版还是第五版,因为第五版把 OpenGL 3.0 里标为 deprecated 的 fixed-pipeline 部分删除了。但是短期内不好说 OS X 对 OpenGL 3.0 能支持到什么程度。而且实际中也要尽量支持 Windows ,那是个对高版本 OpenGL 更严酷的环境。

每次看 OpenGL 都觉得它的编程风格透着股邪气,这当然有我本人经历的缘故,但也主要是因为 OpenGL APIs 对参数和返回值的使用相对一般的编程风格来说比较少,而对当前状态(current context)的依赖相对较多。比如说纹理贴图操作(texture),一般的风格是把作为纹理的位图载入内存,然后返回一个纹理对象的引用或者 handle ;然后调用贴图的函数,把纹理对象和被贴图的对象作为这个函数的参数(或者贴图函数就是被贴图对象的一个方法)。OpenGL 里,一般先用一个函数把位图调入内存创建一个『当前』纹理对象。但是没有任何引用或者 handle 表示『当前』纹理对象,后续的贴图操作仅仅依靠『当前』这个状态本身来操作。再比如向一个多边形添加一个顶点(vertex),没有任何引用表示这个多边形,你能依靠的只是『当前』多边形。当然用过的人对此也不会太奇怪,因为 OpenGL 标准开宗明义 OpenGL 是一个『状态机』模型。

我初次接触 3D 开发是 7 年前的 Java 3D,使用的是和 OpenGL 完全不同的概念模型,虽然用的是 Java 语言,但是更接近声明式的风格。这也是我觉得 OpenGL 邪门的一个原因。

要说这个概念也不是那么难懂,但是为什么会透出一种邪气呢?我猜大概是从高级语言诞生以来,程序员已经习惯了用参数和返回值进行跨函数的信息传递。而高度依赖上下文属于上一代语言,也就是汇编语言的编程风格。

在高级语言中,借助符号化的变量和逻辑化的语法,可以毫不费力的在一行代码里访问五六个甚至十来个变量。所以借助于这些直接访问变量之外的隐式上下文在高级语言里成为了不必要的事情,尤其因为隐式上下文是加大程序局部复杂度的主要原因。而汇编语言则恰恰相反,需要通过好几条甚至几十条指令才能完成高级语言一行的功能,而每条指令只能操作一两个操作数。所以隐式上下文是不可缺少的。

因为采用状态机模型和汇编的编程风格,所以基于 OpenGL 编写的代码颇有些奇异的风格。比如,今天的代码很少在语法结构之外采用缩进,也就是说,除非是语法级别的结构,比如类的定义、分支、循环、函数定义、或者命名空间等等,一般的语句不会采用缩进。而在基于 OpenGL 的代码里,基于上下文变化的缩进是普遍的习惯。OpenGL 虽然提供 C 函数作为 APIs ,但是本质上和形式上都是显卡的汇编语言。在对性能要求极高的 3D 领域,虽然形式上通过 C 语言提供了跨平台能力,本质上却重演了早期计算机开发注重性能的汇编语言阶段。

OpenGL 的汇编风格还我想到一个在网上讨论中遇到过两三次的观点:声明式(declarative)的 DSL(domain specific language)具有广泛的未来,特别是声明式语言给底层平台的全局优化留下了巨大的空间。OpenGL 的目的是描述一个场景(scene),从问题本身来说是一个非常适合声明式 DSL 的任务,从机制来说也需要最大化利用硬件优化。但 OpenGL 并非声明式而是命令式(imperative),而且并非高级语言的过程式结构化风格而是偏重于汇编风格,我想这也是现实向理想低头的一个例子,毕竟只有程序员才能从各个层次考虑优化。