文 / 王越

2011年12月3日,LLVM 3.0正式版发布,完整支持所有ISO C 标准和大部分C 0x的新特性, 这对于一个短短几年的全新项目来说非常不易。

开发者的惊愕

在2011年WWDC(苹果全球开发者大会)的一场与Objective-C相关的讲座上,开发者的人生观被颠覆了。

作为一个开发者,管理好自己程序所使用的内存是天经地义的事,好比人们在溜狗时必须清理狗的排泄物一样(美国随处可见“Clean up after your dogs”的标志)。在本科阶段上C语言的课程时,教授们会向学生反复强调:如果使用malloc函数申请了一块内存,使用完后必须再使用free函数把申请的内存还给系统——如果不还,会造成“内存泄漏”的结果。这对于Hello World可能还不算严重,但对于庞大的程序或是长时间运行的服务器程序,泄内存是致命的。如果没记住,自己还清理了两次,造成的结果则严重得多——直接导致程序崩溃

Objective-C有类似malloc/free的对子,叫alloc/dealloc,这种原始的方式如同管理C内存一样困难。所以Objective-C中的内存管理又增加了“引用计数”的方法,也就是如果一个物件被别的物件引用一次,则引用计数加一;如果不再被该物件引用,则引用计数减一;当引用计数减至零时,则系统自动清掉该物件所占的内存。具体来说,如果我们有一个字符串,当建立时,需要使用alloc方法来申请内存,引用计数则变成了一;然后被其他物件引用时,需要用retain方法去增加它的引用计数,变成二。当它和刚才引用的物件脱离关联时,需使release方法减少引用计数,又变回了一;最后,使用完这个字符串时,再用release方法减少其引用计数,这时,运行库发现其引用计数变为零了,则回收走它的内存。这是手动的方式

这种方式自然很麻烦,所以又设计出一种叫做autorelease的机制(不是类似Java的自动垃圾回收)。在Objective-C中,设计了一个叫做NSAutoReleasePool的池,当开发者需要完成一个任务时(比如每开启一个线程,或者开始一个函数),可以手动创立一个这样的池子, 然后通过显式申明把物件扔进自动回收池中。NSAutoReleasePool内有一个数组来保存声明为autorelease的所有对象。如果一个对象声明为autorelease,则会自动加到池子里。如果完成了一个任务(结束线程了,或者退出那个函数),则开发者需对这个池子发送一个drain消息。这时,NSAutoReleasePool会对池子中所有的物件发送release消息,把它们的引用计数都减一 ——这就好比游泳池关门时通知所有客人都“滚蛋”一样。所以开发者无需显式声明release,所有的物件也会在池子清空时自动呼叫release函数,如果引用计数变成零了,系统才回收那块内存。所以这是个半自动、半手动的方式

Objective-C的这种方式虽然比起C来进了一大步,我刚才花了几分钟就和读者讲明白了。只要遵守上面这两个简单的规则,就可以保证不犯任何错误。但这和后来的Java自动垃圾回收相比则是非常繁琐的,哪怕是再熟练的开发者,一不小心就会弄错。而且,哪怕很简单的代码,比如物件的getter/setter函数,都需要用户写上一堆的代码来管理接收来的物件的内存。

经典教材《Cocoa Programming for Mac OS X》用了整整一章节的篇幅,来讲解Objective-C中内存管理相关的内容,但初学者们看得还是一头雾水。所以,在2007年10.5发布时,Objective-C做出了有史以来最大的更新,最大的亮点是它的运行库libobjc 2.0正式支持自动垃圾回收,也就是由运行库在运行时随时侦测哪些物件需要被释放。听上去很不错,可惜使用这个技术的项目却少之又少。原因很简单,使用这个特性,会有很大的性能损失,使Objective-C的内存管理效率低得和Java一样,而且一旦有一个模块启用了这个特性,这个进程中所有的地方都要启用这个特性——因此如果你写了一个使用垃圾回收的库,那所有引用你库的程序就都得被迫使用垃圾回收。所以Apple自己也不使用这项技术,大量的第三方库也不使用它。

这个问题随Apple在移动市场的一炮走红而变得更加严峻。不过这次,Apple和与会的开发者讲,他们找到了一个解决问题的终极方法,这个方法把从世界各地专程赶来聆听圣谕的开发者惊得目瞪口呆——你不用写任何内存管理代码,也不需要使用自动垃圾回收。因为我们的编译器已经学会了上面所介绍的内存管理规则,会自动在编译程序时把这些代码插进去。

这个编译器,一直是Apple公开的秘密——LLVM。说它公开,是因为它自始至终都是一个开源项目;而秘密,则是因为它从来没公开在WWDC的Keynote演讲上亮相过 。

一直关注这系列连载的读者一定还记得,在第二篇《Linus Torvalds的短视》介绍Apple和GPL社区的不合时,提到过“自以为是但代码又写得差的开源项目,Apple事后也遇到不少,比如GCC编译器项目组。虽然大把钞票扔进去,在先期能够解决一些问题,但时间长了这群人总和Apple过不去,并以自己在开源世界的地位恫吓之,最终Apple由于受不了这些项目组的态度、协议、代码质量,觉得还不如自己造轮子来得方便。”LLVM则是Apple造的这个轮子,它的目的是完全替代掉GCC那条编译链。它的主要作者,则是现在就职于Apple的Chris Lattner。

编译器高材生Chris Lattner

2000年,本科毕业的Chris Lattner像中国多数大学生一样,按部就班地考了GRE,最终前往UIUC(伊利诺伊大学厄巴纳香槟分校),开始了艰苦读计算机硕士和博士的生涯。在这阶段,他不仅周游美国各大景点,更是努力学习科学文化知识,翻烂了“龙书”(《Compilers: Principles, Techniques, and Tools》),成了GPA牛人【注:最终学分积4.0满分】,以及不断地研究探索关于编译器的未知领域,发表了一篇又一篇的论文,是中国传统观念里的“三好学生”。他的硕士毕业论文提出了一套完整的在编译时、链接时、运行时甚至是在闲置时优化程序的编译思想,直接奠定了LLVM的基础。
LLVM在他念博士时更加成熟,使用GCC作为前端来对用户程序进行语义分析产生IF(Intermidiate Format),然后LLVM使用分析结果完成代码优化和生成。这项研究让他在2005年毕业时,成为小有名气的编译器专家,他也因此早早地被Apple相中,成为其编译器项目的骨干。

Apple相中Chris Lattner主要是看中LLVM能摆脱GCC束缚。Apple(包括中后期的NeXT) 一直使用GCC作为官方的编译器。GCC作为开源世界的编译器标准一直做得不错,但Apple对编译工具会提出更高的要求。

一方面,是Apple对Objective-C语言(甚至后来对C语言)新增很多特性,但GCC开发者并不买Apple的帐——不给实现,因此索性后来两者分成两条分支分别开发,这也造成Apple的编译器版本远落后于GCC的官方版本。另一方面,GCC的代码耦合度太高,不好独立,而且越是后期的版本,代码质量越差,但Apple想做的很多功能(比如更好的IDE支持)需要模块化的方式来调用GCC,但GCC一直不给做。甚至最近,《GCC运行环境豁免条款 (英文版)》从根本上限制了LLVM-GCC的开发。 所以,这种不和让Apple一直在寻找一个高效的、模块化的、协议更放松的开源替代品,Chris Lattner的LLVM显然是一个很棒的选择。

刚进入Apple,Chris Lattner就大展身手:首先在OpenGL小组做代码优化,把LLVM运行时的编译架在OpenGL栈上,这样OpenGL栈能够产出更高效率的图形代码。如果显卡足够高级,这些代码会直接扔入GPU执行。但对于一些不支持全部OpenGL特性的显卡(比如当时的Intel GMA卡),LLVM则能够把这些指令优化成高效的CPU指令,使程序依然能够正常运行。这个强大的OpenGL实现被用在了后来发布的Mac OS X 10.5上。同时,LLVM的链接优化被直接加入到Apple的代码链接器上,而LLVM-GCC也被同步到使用GCC4代码。

LLVM真正的发迹,则得等到Mac OS X 10.6 Snow Leopard登上舞台。可以说, Snow Leopard的新功能,完全得益于LLVM的技术。而这一个版本,也是将LLVM推向真正成熟的重大机遇。

关于Snow Leopard的三项主推技术(64位支持、OpenCL,以及Grand Central Dispatch)的细节,我们会在下一次有整整一期篇幅仔细讨论,这次只是点到为止——我们告诉读者,这些技术,不但需要语言层面的支持(比如Grand Centrual Dispatch所用到的“代码块”语法, 这被很多人看作是带lambda的C),也需要底层代码生成和优化(比如OpenCL是在运行时编译为GPU或CPU代码并发执行的)。而这些需求得以实现,归功于LLVM自身的新前端——Clang。

优异的答卷——Clang

前文提到,Apple吸收Chris Lattner的目的要比改进GCC代码优化宏大得多——GCC系统庞大而笨重,而Apple大量使用的Objective-C在GCC中优先级很低。此外GCC作为一个纯粹的编译系统,与IDE配合得很差。加之许可证方面的要求,Apple无法使用LLVM 继续改进GCC的代码质量。于是,Apple决定从零开始写 C、C 、Objective-C语言的前端 Clang,完全替代掉GCC。

正像名字所写的那样,Clang只支持C,C 和Objective-C三种C家族语言。2007年开始开发,C编译器最早完成,而由于Objective-C相对简单,只是C语言的一个简单扩展,很多情况下甚至可以等价地改写为C语言对Objective-C运行库的函数调用,因此在2009年时,已经完全可以用于生产环境。C 的支持也热火朝天地进行着。

Clang的加入代表着LLVM真正走向成熟和全能,Chris Lattner以影响他最大的“龙书”封面【注:见http://en.wikipedia.org/wiki/Dragon_Book_(computer_science)】为灵感,为项目选定了图标——一条张牙舞爪的飞龙

Clang一个重要的特性是编译快速,占内存少,而代码质量还比GCC来得高。测试结果表明Clang编译Objective-C代码时速度为GCC的3倍【注:http://llvm.org/pubs/2007-07-25-LLVM-2.0-and-Beyond.pdf】,而语法树(AST)内存占用则为被编译源码的1.3倍,而GCC则可以轻易地可以超过10倍。Clang不但编译代码快,对于用户犯下的错误,也能够更准确地给出建议。使用过GCC的读者应该熟悉,GCC给出的错误提示基本都不是给人看的。

比如最简单的:

struct foo { int x; }
typedef int bar;

如果使用GCC编译,它将告诉你:
t.c:3: error: two or more data types in declaration specifiers

但是Clang给出的出错提示则显得人性化得多:
t.c:1:22: error: expected “;” after struct

甚至,Clang可以根据语境,像拼写检查程序一样地告诉你可能的替代方案。
比如这个程序:

#include <inttypes.h>
int64 x;

GCC一样给出乱码似的出错提示:

t.c:2: error: expected “=”, “,”, “;”, “asm” or “__attribute__” before “x”

而优雅的Clang则用彩色的提示告诉你是不是拼错了,并给出可能的变量名:

t.c:2:1: error: unknown type name “int64″; did you mean “int64_t”?
int64 x;^~~~~int64_t

更多的例子可以参考http://blog.llvm.org/2010/04/amazing-feats-of-clang-error-recovery.html。 而同时又因为Clang是高度模块化的一个前端,很容易实现代码的高度重用。所以比如Xcode 4.0的集成编程环境就使用Clang的模块来实现代码的自动加亮、代码出错的提示和自动的代码补全。开发者使用Xcode 4.0以后的版本,可以极大地提高编程效率,尽可能地降低编译错误的发生率。

支持C 也是Clang的一项重要使命。C 是一门非常复杂的语言,大多编译器(如GCC、MSVC)用了十多年甚至二十多年来完善对C 的支持,但效果依然不很理想。Clang的C 支持却一直如火如荼地展开着。2010年2月4日,Clang已经成熟到能自举(即使用Clang编译Clang,到我发稿时,LLVM 3.0发布已完整支持所有ISO C 标准,以及大部分C 0x的新特性

这对于一个短短几年的全新项目来说是非常不易的。得益于本身健壮的架构和Apple的大力支持,Clang越来越全能,从FreeBSD【注:http://lists.freebsd.org/pipermail/freebsd-current/2009-February/003743.html】 到Linux Kernel【注:http://lists.cs.uiuc.edu/pipermail/cfe-dev/2010-October/011711.html】, 从Boost【注:http://blog.llvm.org/2010/05/clang-builds-boost.html】 到Java虚拟机, Clang支持的项目越来越多。

Apple的Mac OS X以及iOS也成了Clang和LLVM的主要试验场——10.6时代,很多需要高效运行的程序比如OpenSSL和Hotspot就由LLVM-GCC编译来加速的。而10.6时代的Xcode 3.2诸多图形界面开发程序如Xcode、Interface Builder等,皆由Clang编译。到了Mac OS X 10.7,整个系统的的代码都由Clang或LLVM-GCC编译【注:http://llvm.org/Users.html】。

LLVM周边工具

由于受到Clang项目的威胁,GCC也不得不软下来,让自己变得稍微模块化一些,推出插件的支持,而LLVM项目则顺水推舟,索性废掉了出道时就一直作为看家本领的LLVM-GCC,改为一个GCC的插件DragonEgg。 Apple也于Xcode 4.2彻底抛弃了GCC工具链。

而Clang的一个重要衍生项目,则是静态分析工具,能够通过自动分折程序的逻辑,在编译时就找出程序可能的bug。在Mac OS X 10.6时,静态分析被集成进Xcode 3.2,帮助用户查找自己犯下的错误。其中一个功能,就是告诉用户内存管理的Bug,比如alloc了一个物件却忘记使用release回收。这已经是一项很可怕的技术,而Apple自己一定使用它来发现并改正Mac OS X整个系统各层面的问题。但许多开发者还不满足——既然你能发现我漏写了release,你为什么不能帮我自动加上呢?于是ARC被,发生了文章开头开发者们的惊愕——从来没有人觉得这件事是可以做成的。

除LLVM核心和Clang以外,LLVM还包括一些重要的子项目,比如一个原生支持调试多线程程序的调试器LLDB,和一个C 的标准库libc ,这些项目由于是从零重写的,因此要比先前的很多项目站得更高,比如先前GNU、Apache、STLport等C 标准库在设计时,C 0x标准还未公布,所以大多不支持这些新标准或者需要通过一些肮脏的改动才能支持,而libc 则原生支持C 0x。而且在现代架构上,这些项目能动用多核把事情处理得更好。

不单单是Apple,诸多的项目和编程语言都从LLVM里取得了关键性的技术。Haskell语言编译器GHC使用LLVM作为后端,实现了高质量的代码编译。很多动态语言实现也使用LLVM作为运行时的编译工具,较著名的有Google的Unladen Swallow【注:Python实现,后夭折】、PyPy【注:Python实现】,以及MacRuby【注:Ruby实现】。例如 MacRuby 后端改为LLVM后,速度不但有了显著的提高,更是支持Grand Central Dispatch来实现高度的并行运行。由于LLVM高度的模块化,很方便重用其中的组件来作为一个实现的重要组成部分,因此类似的项目会越来越多。

LLVM的成熟也给其他痛恨GCC的开发项目出了一口恶气。其中最重要的,恐怕是以FreeBSD为代表的BSD社区。BSD社区和Apple的联系一向很紧密,而且由于代码相似,很多Apple的技术如Grand Central Dispatch也是最早移植到FreeBSD上。BSD社区很早就在找GCC的替代品,无奈大多都很差(如Portable C Compiler产生的代码质量和gcc不能同日而语)。

一方面是因为不满意GCC的代码品质【注:BSD代码整体要比GNU的高一些,GNU代码永无休止地出现各种严重的安全问题】,更重要的是协议问题。BSD开发者有洁癖的居多,大多都不喜欢GPL代码,尤其是GPL协议第三版发布时,和FreeBSD的协议甚至是冲突的。这也正是为什么FreeBSD中包含的GNU的C 运行库还是2007年以GPLv2发布的老版本,而不是支持C 0x的但依GPLv3协议发布的新版本。 因此历时两年的开发后,2012年初发布的FreeBSD 9.0中,Clang被加入到FreeBSD的基础系统。 但这只是第一步,因为FreeBSD中依然使用GNU的C STL 库、C 运行库、GDB调试器、libgcc/libgcc_s编译库都是和编译相关的重要底层技术,先前全被GNU垄断,而现在LLVM子项目lldb、libc 、compiler-rt等项目的出现,使BSD社区有机会向GNU说“不”,因此一个把GNU组件移出FreeBSD的计划被构想出来,并完成了很大一部分。编写过《Cocoa Programming Developer”s Handbook》的著名Objective-C牛人David Chisnall也被吸收入FreeBSD开发组完成这个计划的关键部分。 预计在FreeBSD 10发布时,将不再包含GNU代码。

LLVM在短短五年内取得的快速发展充分反映了Apple对于产品技术的远见和处理争端的决心和手腕,并一跃成为最领先的开源软件技术。而Chris Lattner在2010年也赢得了他应有的荣誉——Programming Languages Software Award(程序设计语言软件奖)

作者王越,美国宾夕法尼亚大学计算机系研究生,中国著名TeX开发者,非著名OpenFOAM开发者。