LLVM与Clang-开发者的惊愕

文 / 王越

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

开发者的惊愕

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

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

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

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

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(伊利诺伊大学厄巴纳香槟分校),开始了艰苦读计算机硕士和博士的生涯。在这阶段,他不仅周游美国各大景点4,更是努力学习科学文化知识,翻烂了“龙书”(《Compilers: Principles, Techniques, and Tools》),成了GPA牛人5【注:最终学分积4.0满分】,以及不断地研究探索关于编译器的未知领域,发表了一篇又一篇的论文,是中国传统观念里的“三好学生”。他的硕士毕业论文提出了一套完整的在编译时、链接时、运行时甚至是在闲置时优化程序的编译思想6,直接奠定了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的代码耦合度太高,不好独立,而且越是后期的版本,代码质量越差7,但Apple想做的很多功能(比如更好的IDE支持)需要模块化的方式来调用GCC,但GCC一直不给做。甚至最近,《GCC运行环境豁免条款 (英文版)8》从根本上限制了LLVM-GCC的开发。 所以,这种不和让Apple一直在寻找一个高效的、模块化的、协议更放松的开源替代品,Chris Lattner的LLVM显然是一个很棒的选择。

刚 进入Apple,Chris Lattner就大展身手:首先在OpenGL小组做代码优化,把LLVM运行时的编译架在OpenGL栈上,这样OpenGL栈能够产出更高效率的图形 代码。如果显卡足够高级,这些代码会直接扔入GPU执行。但对于一些不支持全部OpenGL特性的显卡(比如当时的Intel GMA卡),LLVM则能够把这些指令优化成高效的CPU指令,使程序依然能够正常运行9。这个强大的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所用到的“代码块”语法10, 这被很多人看作是带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年开始开发11,C编译器最早完成,而由于Objective-C相对简单,只是C语言的一个简单扩展,很多情况下甚至可以等价地改写为C语言对Objective-C运行库的函数调用,因此在2009年时,已经完全可以用于生产环境。C++的支持也热火朝天地进行着。

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

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倍14。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的新特性15

这对于一个短短几年的全新项目来说是非常不易的。得益于本身健壮的架构和Apple的大力支持,Clang越来越全能,从FreeBSD16【注:http://lists.freebsd.org/pipermail/freebsd-current/2009-February/003743.html】 到Linux Kernel17【注:http://lists.cs.uiuc.edu/pipermail/cfe-dev/2010-October/011711.html】, 从Boost18【注:http://blog.llvm.org/2010/05/clang-builds-boost.html】 到Java虚拟机19, 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也不得不软下来,让自己变得稍微模块化一些,推出插件的支持20,而LLVM项目则顺水推舟,索性废掉了出道时就一直作为看家本领的LLVM-GCC,改为一个GCC的插件DragonEgg21。 Apple也于Xcode 4.2彻底抛弃了GCC工具链。

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

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

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

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

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

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

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

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

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


References

  1. ^直接导致程序崩溃 (blog.delphij.net)
  2. ^手动的方式 (developer.apple.com)
  3. ^半自动、半手动的方式 (developer.apple.com)
  4. ^他不仅周游美国各大景点 (photos.nondot.org)
  5. ^成了GPA牛人 (nondot.org)
  6. ^硕士毕业论文提出了一套完整的在编译时、链接时、运行时甚至是在闲置时优化程序的编译思想 (www.programmer.com.cn)
  7. ^越是后期的版本,代码质量越差 (www.thejemreport.com)
  8. ^GCC运行环境豁免条款 (英文版) (blog.delphij.net)
  9. ^LLVM则能够把这些指令优化成高效的CPU指令,使程序依然能够正常运行(lists.cs.uiuc.edu)
  10. ^Grand Centrual Dispatch所用到的“代码块”语法 (lists.cs.uiuc.edu)
  11. ^2007年开始开发 (lists.cs.uiuc.edu)
  12. ^“龙书”封面 (en.wikipedia.org)
  13. ^一条张牙舞爪的飞龙 (llvm.org)
  14. ^GCC则可以轻易地可以超过10倍 (llvm.org)
  15. ^LLVM 3.0发布已完整支持所有ISO C++标准,以及大部分C++ 0x的新特性 (clang.llvm.org)
  16. ^FreeBSD (lists.freebsd.org)
  17. ^Linux Kernel (lists.cs.uiuc.edu)
  18. ^Boost (blog.llvm.org)
  19. ^Java虚拟机 (weblogs.java.net)
  20. ^插件的支持 (gcc.gnu.org)
  21. ^GCC的插件DragonEgg (dragonegg.llvm.org)
  22. ^静态分析工具 (clang-analyzer.llvm.org)
  23. ^release回收 (clang-analyzer.llvm.org)
  24. ^集成进Clang (www.programmer.com.cn)
  25. ^原生支持调试多线程程序的调试器LLDB (lldb.llvm.org)
  26. ^C++的标准库libc++ (libcxx.llvm.org)
  27. ^Haskell语言编译器GHC使用LLVM作为后端 (blog.llvm.org)
  28. ^Python实现,后夭折 (code.google.com)
  29. ^Python实现 (codespeak.net)
  30. ^Ruby实现 (www.programmer.com.cn)
  31. ^例如 MacRuby 后端改为LLVM后,速度不但有了显著的提高 (programmingzen.com)
  32. ^BSD社区很早就在找GCC的替代品 (en.wikipedia.org)
  33. ^安全问题 (blog.delphij.net)
  34. ^GPL协议第三版发布时,和FreeBSD的协议甚至是冲突的 (www.freebsdfoundation.org)
  35. ^Clang被加入到FreeBSD的基础系统 (wiki.freebsd.org)
  36. ^GNU组件移出FreeBSD的计划被构想出来 (wiki.freebsd.org)
  37. ^本文选自《程序员》杂志2012年01期,更多精彩内容敬请关注01期杂志(www.programmer.com.cn)
  38. ^《程序员》2012年杂志订阅送好礼活动火热进行中 (dingyue.programmer.com.cn)
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值