十二 31

文 / 王越

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开发者。

 Mac OS X 背后的故事系列更多精彩内容

本文选自《程序员》杂志2012年01期,更多精彩内容敬请关注01期杂志

《程序员》2012年杂志订阅送好礼活动火热进行中

转播到腾讯微博

----->立刻申请加入《程序员》杂志读者俱乐部,与杂志编辑直接交流,参与选题,优先投稿

28 Responses to “Mac OS X 背后的故事(八)三好学生Chris Lattner的LLVM编译工具链”

  1. zenny Chen 说道:

    非常棒!佩服chris

  2. carlshen 说道:

    曾经对GCC很崇拜,现在看来LLVM很牛,有点崇拜,有机会学习学习。

  3. Ralph Zhang 说道:

    写得挺好,揪个小错:libstdc++ 是主要配合 gcc 的 C++ 标准库,配合 clang 的那个叫做 libc++

  4. Yue Wang 说道:

    @Ralph Zhang 多謝!我的文章確實寫錯了,在此向讀者道歉。也望編輯更正。

  5. json007 说道:

    一直追求完美

  6. 嘉扬 说道:

    写得确实好!赞

  7. Mike 说道:

    我2005年用Linux的时候,那时候gcc就要插件支持的。只是耦合度非常高,需要重新编译gcc。
    还有pypy只是提供了llvm的target而已,而非使用llvm。记得他们的作者吐槽过llvm的jit实际上被设计的还是一个静态的优化器,很多优化做不了。

  8. newguest 说道:

    看了文章以为是中国人搞的,再仔细一查不是这么回事。
    http://nondot.org/sabre/
    误导啊

  9. magisu 说道:

    言过其实。

    LLVM还远远不如GCC成熟。我刚刚试了试Clang编译我的表达式模板,就不工作。

  10. Yue Wang 说道:

    @newguest

    > 看了文章以为是中国人搞的,再仔细一查不是这么回事。

    我原文寫了他是中國人了嗎?看人名也不是啊⋯⋯

    @magisu

    > LLVM还远远不如GCC成熟。我刚刚试了试Clang编译我的表达式模板,就不工作。

    我原文已經寫了LLVM-Clang的C/ObjC已經production,而C++支援還不完整。
    另外gcc能編譯而clang不能編譯並不代表你的模板是符合ISO C++的,雖然clang盡可能兼容gcc,但不代表它要做得和gcc一模一樣。

  11. magisu 说道:

    @yue wang

    我的模板是符合标准的,intel的编译器还有digit mars的编译器都支持,你可以找比如Modern C++ Design或者C++ Template the complete set中的代码试试。你一会说c++支持不完整一会又说完全支持cxx03里面的,我不懂你的意思。

  12. magisu 说道:

    另外LLVM的有趣之处在于它对优化号称是JIT的。我很有兴趣但是作者却不重视这个。Low Level Virtural Machine最重要的是它是一个Virtual Machine-based。

  13. Yue Wang 说道:

    @magisu

    多處提到了,只是沒用jit這個詞而已

  14. magisu 说道:

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

  15. hikui 说道:

    我记得llvm的检查是否release过的功能只能在一个文件内检查,跨文件情况它是无能为力的。

  16. chenwj 说道:

    LLVM 跟 “Low Level Virtural Machine” 已經沒有關係,那是歷史遺跡了。簡言之,LLVM 跟 Virtural Machine 沒有關係。

    http://lists.cs.uiuc.edu/pipermail/llvmdev/2011-December/046443.html

  17. [...] 最近拿了朋友的MacBook Pro来玩,发现其成功的确有其原因。因为最近都在搞RoR,使用TextMate应该是最好的选择了。我也利用一些时间来研究了这电脑杂七杂八的东西,包括其中一些另类的操作系统理念。 MAC OS的界面就不用多说了,其实给我最重要的感觉是简单易用,相比WIN,更能让自己专注于工作(对我个人而言)。总得来说因为时间很短,暂时也没什么可以分享的,不过刚看一篇好文,分享给大家:Mac OS X 背后的故事(八)三好学生Chris Lattner的LLVM编译工具链 Geekllvm, mac ← 你会爱上的一个Notepad++插件——FingerText [...]

  18. Lig 说道:

    试了LLVM3.0+clang,编译速度的确比Gcc快,出错提示也更友好,但是clang的优化有问题,我的程序(大约5万行)在clang编译以后可以正常运行,但是加上O2优化,运行就出错,使用Gcc就没有这个问题。

  19. Yue Wang 说道:

    > 但是clang的优化有问题,我的程序(大约5万行)在clang编译以后可以正常运行,但是加上O2优化,运行就出错,使用Gcc就没有这个问题。

    優化導致程序運行出錯是很正常的,我使用GCC有近十年時間了,遇到這類問題不計其數。使用Unit Test來測試函數的正確性,不要過度信任編譯器。 如果Unit Test 顯示某一個函數被優化錯了,可以使用 #pragma 暫時禁止對那個函數的優化。

  20. test 说道:

    很好,看到评论,有问题可以讨论共同进步,带着偏见就不好了

  21. zp 说道:

    长见识了,Chris Lattner真个是非常的牛的人,不仅仅是底层的汇编,而且还有图像处理,模拟器.膜拜.

  22. Yue Wang 说道:

    clang fully supports c++11 starting from today.

  23. karas 说道:

    llvm至今效率不如gcc

  24. karas 说道:

    这系列文章越来越觉得太果粉。不够客观公正。

  25. viva 说道:

    c++ 代码llvm/clang在执行速度上还是没有明显优势啊。

    http://www.phoronix.com/scan.php?page=article&item=llvm_clang33_3way&num=4

    For the Intel systems with the Apache web-server the LLVM/Clang 3.3 performance appears slightly faster.

    Overall, LLVM/Clang 3.3 appears to be largely competitive with GCC 4.8.0 except in select cases like the missing OpenMP support. Depending upon the hardware, there are some nice performance improvements with LLVM 3.3 over its predecessor. Aside from performance changes, LLVM 3.3 is nice in that the Clang compiler is C++11 feature-complete, there is now the ARM AArch64 64-bit support, IBM SystemZ support, there’s the SLP Vectorizer and improvements to the Loop Vectorizer, the AMD R600 GPU LLVM back-end has been merged, and much more. Look for the LLVM/Clang 3.3 final release in early June

  26. whatever 说道:

    @Lig 我编了10万行代码llvm没问题,你可能代码行数不够

  27. 特酷吧 说道:

    这系列的文章非常不错,把历史的来龙去脉讲清楚,有助于更深刻的理解技术原理

请评论

preload preload preload
京ICP备06065162