三好學生Chris Lattner的LLVM編譯工具鏈

2011年12月3日,LLVM 3.0正式版發佈,完整支持全部ISO C++標準和大部分C++ 0x的新特性, 這對於一個短短几年的全新項目來講很是不易。 html

開發者的驚愕 前端

在2011年WWDC(蘋果全球開發者大會)的一場與Objective-C相關的講座上,開發者的人生觀被顛覆了。 java

做爲一個開發者,管理好本身程序所使用的內存是天經地義的事,比如人們在溜狗時必須清理狗的排泄物同樣(美國隨處可見「Clean up after your dogs」的標誌)。在本科階段上C語言的課程時,教授們會向學生反覆強調:若是使用malloc函數申請了一塊內存,使用完後必須再使用free函數把申請的內存還給系統——若是不還,會形成「內存泄漏」的結果。這對於Hello World可能還不算嚴重,但對於龐大的程序或是長時間運行的服務器程序,泄內存是致命的。若是沒記住,本身還清理了兩次,形成的結果則嚴重得多——直接致使程序崩潰c++

Objective-C有相似malloc/free的對子,叫alloc/dealloc,這種原始的方式如同管理C內存同樣困難。因此Objective-C中的內存管理又增長了「引用計數」的方法,也就是若是一個物件被別的物件引用一次,則引用計數加一;若是再也不被該物件引用,則引用計數減一;當引用計數減至零時,則系統自動清掉該物件所佔的內存。具體來講,若是咱們有一個字符串,當創建時,須要使用alloc方法來申請內存,引用計數則變成了一;而後被其餘物件引用時,須要用retain方法去增長它的引用計數,變成二。當它和剛纔引用的物件脫離關聯時,需使release方法減小引用計數,又變回了一;最後,使用完這個字符串時,再用release方法減小其引用計數,這時,運行庫發現其引用計數變爲零了,則回收走它的內存。這是手動的方式web

這種方式天然很麻煩,因此又設計出一種叫作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和與會的開發者講,他們找到了一個解決問題的終極方法,這個方法把從世界各地專程趕來聆聽聖諭的開發者驚得目瞪口呆——你不用寫任何內存管理代碼,也不須要使用自動垃圾回收。由於咱們的編譯器已經學會了上面所介紹的內存管理規則,會自動在編譯程序時把這些代碼插進去。 xcode

這個編譯器,一直是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被集成進Clang,發生了文章開頭開發者們的驚愕——歷來沒有人以爲這件事是能夠作成的。

除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開發者。

相關文章
相關標籤/搜索