Archive for the ‘软件开发’ Category

高级动态语言与软件业

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 ,繁复的参数和数据结构转换。如果这一切有所改观,那么高级动态语应该可以释放更强大的生产力。

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. 某些创造性工作或者人脑结构更适合解决的问题还能让人类保持一种优越感和尊严。

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/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 子系统面临类似的程度稍轻的窘境。)

从量子计算到 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),而且并非高级语言的过程式结构化风格而是偏重于汇编风格,我想这也是现实向理想低头的一个例子,毕竟只有程序员才能从各个层次考虑优化。