天天看點

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)
上一篇: hdfs糾删碼
下一篇: 日語輸入法