Archive for 2011年3月

逻辑的残影

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