編譯器的「五個十年」發展史
自20世紀70年代就讀伊利諾伊大學研究生院以來,Michael Wolfe就一直主攻平行計算方面的語言和編譯器。在此過程中,他與別人創辦Kuck and Associates(已被英特爾收購),在俄勒岡州研究生院(自與俄勒岡健康與科學大學合併以來)投身學術界,並在PGI開發高效能Fortran(PGI先被意法半導體收購,最近被英偉達收購)。如今大部分時間,他在一個為高度平行計算、尤其是為英偉達GPU加速器開發和改進PGI編譯器的團隊擔任技術主管。本文由機器之心經授權轉載自 ofollow,noindex" target="_blank">雲頭條(ID:YunTouTiao ) ,未經授權禁止二次轉載。
如果想了解我們在計算機架構和驅動計算機架構的編譯器方面的現狀,有必要看看編譯器如何在六十年間由一種架構改用另一種架構。
先讓目光回到1957年的第一個編譯器IBM Fortran。這是一項了不起的技術。如果你看看它的起源和取得的成果,付出的巨大努力是今天的人都無法想象的。
IBM想要銷售計算機,想要銷售讓更多的人能夠進行程式設計的計算機。當時程式設計是用匯編語言完成的。這太難了,IBM很清楚這一點。於是藍色巨人希望人們有辦法更快地編寫程式,又不犧牲效能。Fortran的開發人員(指發明Fortran的那些人,而不是發明使用編譯器和語言的程式的那些人)希望利用如今所謂的高階程式語言編寫的程式,提供儘可能接近手動調整的機器程式碼的效能。
說到編譯器,你必須考慮三個P:效能、生產力和可移植性。Fortran的發明者拿機器程式碼的效能作了比較。生產力方面的好處是,程式設計師再也不必編寫機器程式碼。我不知道IBM在可移植性方面的最初意圖,不過你在1957年無法為軟體獲得專利權,IBM也沒有抱怨其他企業組織實現Fortran。因此沒過多久,市面上出現了來自其他供應商的面向其他機器的Fortran編譯器。這立即為Fortran程式提供了機器程式碼無法想象的可移植性。
從這一刻起,編譯器開始迅猛發展起來。讓我們看看每個十年的情況。
第一個十年
在20世紀60年代(就在我出生前),計算機架構師和編譯器編寫者首先開始考慮並行性。即使那樣,人們仍然認為計算機速度不夠快,速度提升也不夠快,覺得並行性有望解決這個問題。我們看到指令級並行性引入到了Seymour Cray公司的CDC 6600和CDC 7600以及IBM System/ 360 Model 91中。更為激進的做法是開發出了由伊利諾伊大學的研究人員設計並由Burroughs公司製造的ILLIAC IV、Control Data Corp STAR-100以及德州儀器(TI)Advanced Scientific Computer。CDC和TI的系統是記憶體到記憶體的長向量機,而ILLIAC IV是我們今天所說的SIMD機器。ILLIAC之所以功能有限,是由於沒有主要的標量處理器,程式設計起來確實很難。面向STAR-100的Fortran編譯器添加了用於描述長連續向量操作的語法。TI ASC機器最值得關注,因為它擁有第一個自動向量化編譯器。TI做了一番了不起的工作,確實提升了當時編譯器分析的最新水平。
第二個十年
20世紀70年代,Cray-1成為第一臺商業上大獲成功的超級計算機。它有眾多的跟隨者和模仿者,許多讀者可能瞭如指掌。Cray機器的成功很大程度上歸功於引入了向量暫存器。就像標量暫存器一樣,向量暫存器讓程式可以對小小的資料向量執行許多操作,沒必要從記憶體載入和儲存到記憶體。Cray Research還開發了一種非常大膽的向量化編譯器。它在許多方面與早期的TI編譯器類似,但Cray編譯器擁有讓它非常備受關注的附加功能。其中最重要的一項功能是能夠為程式設計師提供編譯器反饋。
如今開發人員編譯程式時,如果程式含有語法錯誤,編譯器將生成出錯訊息。程式設計師不斷修復這些錯誤並重新編譯,直到擁有一個正常執行的程式。如果開發人員希望程式執行得更快,可以啟用編譯器優化標誌,好讓生成的可執行檔案執行得更快——他們希望如此。在大多數計算機上,優化程式碼和非優化程式碼之間的效能可能相差兩倍,通常差異小得多,比如相差10%、20%或者50%。與之相比的是原始Cray機器上可用的向量指令集。在這種情況下,程式碼被Cray編譯器優化和向量化後,程式設計師常常會看到效能提升5倍到10倍。程式設計師、尤其是高效能運算(HPC)程式設計師願意做大量工作,以便將效能提升5倍或更高。
來自Cray編譯器的反饋將告訴程式設計師它在第110行向量化了一個迴圈(loop),在第220行向量化了另一個迴圈,但是沒有在第230行向量化迴圈,原因是第235行有無法被向量化的I/O語句或函式呼叫。或者,可能存在編譯器無法分析的陣列引用,或者某個陣列的第二個下標中的一個未知變數阻礙了依賴項分析,因此該迴圈無法進行向量化。想要獲得向量效能的Cray程式設計師格外注意這些訊息。他們根據這種反饋修改了程式碼,可能從迴圈中取出I/O語句,或者將迴圈推入到子程式,或者修改陣列引用以刪除某個未知變數。有時他們會新增一個編譯器指令,以便向編譯器傳達可以安全地進行向量化這一資訊,即使編譯器無法通過依賴項分析來確定這一點。
多虧了編譯器的反饋,發生了三件事。
首先,更多的程式被向量化,更多的程式設計師得益於Cray向量效能。其次,Cray程式設計師受過了培訓,不再將I/O語句、條件語句和過程呼叫放入到迴圈的中間。他們明白一個步長(stride)的資料訪問很重要,確保內部迴圈中的陣列訪問是一個步長。第三,用Cray編譯器自動向量化的程式可以在來自Alliant、Convex、Digital、富士通、日立、IBM和NEC的許多類似的向量機上重新編譯。所有這些系統都擁有帶向量暫存器的向量處理器和自動向量化編譯器,之前針對Cray優化的程式碼在所有這些機器上都可以進行向量化,並很順暢地執行。簡而言之,Cray程式設計師終於實現了效能、生產力和可移植性這三個目標。
這個編譯器反饋有多重要、培訓整整一代HPC開發人員為向量機程式設計有多成功,怎麼強調都不為過。每個使用它的人都很高興。那時候,我還在伊利諾伊大學,我們考慮開一家立足於並行化編譯器技術的公司。當然,我們的技術比別人的要好,因為我們是一流的學者。在Cray編譯器的早期階段,使用者抱怨編譯器無法對任何內容進行向量化,因而不得不重寫程式碼。我們認為自己有望解決這些問題,於是開了這家小公司專門搞這一塊。幾年後,我們接觸同樣那樣使用者、展示我們用於並行化和向量化迴圈的工具時,他們回覆自己不需要這類工具,因為Cray編譯器已經向量化了所有迴圈。倒不是說Cray編譯器變得更聰明,不過我確信它會漸漸變得更好。主要是程式設計師在如何編寫可向量化的迴圈方面訓練有素。
第三個十年
20世紀80年代多處理得到了廣泛的實施和使用。早些時候已有多處理器,包括IBM System /370s和Burroughs系統。但是32位單片微處理器的問世推動多處理技術進入了主流。要開一家計算機公司,你不再需要設計處理器——可以徑直購買。你沒必要編寫作業系統,可以購買Unix的許可。只需要有人全部組裝起來、貼上銘牌。要是有編譯器就好了。 Sequent、Encore和SGI都構建了有一個微處理器、效能出色的系統,但如果可以讓一批處理器並行執行,那就更好了。
不像大獲成功的自動向量化,自動並行化基本上一敗塗地。它適用於最內層迴圈,但要實現大幅的並行加速,通常需要對外層迴圈進行並行化。不過當然,外層迴圈增添了控制流的複雜性,常常包括過程呼叫,現在你的編譯器分析完全崩潰了。一種確實可行的方法是,讓程式設計師參與其中,分析並行性,並輸入編譯器指令來驅動它。我們由此看到了面向並行迴圈的各種指令集紛紛出現。Cray、Encore、IBM和Sequent都有各自的指令集,唯一的共同點就是SGI採用Sequent指令。同樣,許多可擴充套件的系統要傳遞網路訊息,這促使開發了眾多針對特定供應商的、學術性的訊息傳遞庫。
第四個十年
20世紀90年代,所有那些訊息傳遞庫都被訊息傳遞介面(MPI)取而代之,所有那些針對特定供應商的並行化指令都被OpenMP取而代之。出現了擴充套件性更強的並行系統,比如Thinking Machines CM-5以及有成千上萬個商用微處理器的其他系統。基於微處理器的可擴充套件系統主要用MPI進行程式設計,但MPI是一個庫,對編譯器來說不透明。MPI作為一種程式設計模型而具有的優點是,雖然通訊開銷很大,但通訊在程式中是完全暴露的。MPI程式設計師知道何時插入顯式通訊呼叫,他們不遺餘力地儘可能減少和優化通訊。缺點是,從程式設計模型的角度來看,它非常低階,MPI程式設計師沒有得到編譯器的幫助。
OpenMP誕生於針對特定供應商的並行指令集百花齊放的時期,因為使用者需要它。使用者們才不願僅僅為了能在可供使用的所有不同系統上執行程式而將程式重寫12次。OpenMP的整個宗旨就是以同樣的方式彰顯“並行性可以”。不像MPI,編譯器必須支援指令,因為指令是語言的一部分。你無法將OpenMP作為一個庫來實現。
同樣在20世紀90年代,市面上出現了面向單晶片微處理器的SIMD指令集,比如來自英特爾的SSE和SSE2,我們看到編譯器恢復了當時已有20年或25年曆史的同樣的向量化技術,以便自動利用那些SIMD指令。
第五個十年
2000年後不久,眾多供應商開始普遍提供多核微處理器。全世界突然意識到必須為大家解決這個並行程式設計問題。
當時,許多應用程式在一塊晶片上的所有核心上以及分散式記憶體節點上使用扁平的MPI程式設計模型。你始終可以新增更多的MPI序號(rank)來使用更高的並行性。這一招效果有限,但是你開始獲得大量序號時,一些MPI程式會遇到擴充套件問題。資料在每個MPI序號中複製時,記憶體使用會有點失控。一旦你開始在每個節點上放置一二十個核心,複製的每一項資料可供使用的記憶體量是12倍或24倍。如果是為單個節點程式設計,OpenMP及其他共享記憶體並行程式設計模型開始受到追捧。比如在20世紀90年代,英特爾推出了執行緒構建模組(又名TBB);有人聲稱,TBB是如今最受歡迎的並行程式語言。
大概在同一時期,異構HPC系統開始出現,不過異構性其實不是新話題。我們在20世紀60年代就遇到了異構性,使用附加的協處理器用於陣列處理。附加的處理器大受歡迎,原因是它們可以比CPU更快地完成專門操作。它們常常能夠以小型機的價格提供大型機的效能,大約15年來浮點系統在這方面做得很好。在21世紀初期,有幾款專門為HPC市場開發或加以改造的加速器,比如ClearSpeed加速器和IBM Cell處理器。這兩款產品都取得了一些真正的技術成功,但是HPC市場規模太小,無法支援開發獨立的定製處理器晶片。
這給GPU計算留下了缺口。GPU相對定製加速器的優勢在於,GPU的主打功能很好——早期的主要任務是圖形和遊戲,現在還包括AI和深度學習,因此從最新矽片技術和驅動矽片所需的軟體方面來看,開發處理器非常高昂的成本維持得下去。
伴隨異構性而來的是這個顯而易見的問題:我們如何為這些系統程式設計?早在那時,浮點系統提供了在底層使用加速器的子程式庫。程式設計師呼叫該庫,HPC使用者可以高效地使用加速器,無需實際程式設計(不過有一些程式設計師進行了程式設計)。今天,想支援多年來在標量、向量和可擴充套件系統上開發的眾多應用程式,HPC開發人員需要能夠為加速器高效地程式設計,他們希望程式看起來儘可能正常。使用OpenCL或CUDA可以為你提供了很強的控制性,但是對於現有HPC原始碼的影響可能非常大。程式設計師通常將有待在加速器上執行的程式碼提取到特別註釋的函式中,而且編寫的方式常常與為主機編寫的方式全然不同。這時候,專門為通用並行程式設計設計的基於指令的程式設計模型和語言就有一些優勢,而優秀的優化編譯器大有用場。
置身於平行世界
這給我們引出了另一個至關重要的方面。20世紀60年代為指令級並行性開發的所有技術現在都在微處理器裡面。20世紀70年代的向量處理概念存在於微處理器裡面的SIMD暫存器中。20世紀80年代Cray和基於商用處理器的系統的多個處理器都以多個核心的形式存在於微處理器裡面。今天的微處理器可以說整合了過去50年來所做的全部架構工作,而由於我們現在擁有出色的電晶體技術(數十億個門),我們可以這麼做。另外我們現在還有異構性;在一些情況下,我們甚至可以在同一塊晶片上做到異構性。比如說,中國太湖之光系統中的“神威”晶片從封裝的角度來看是一塊晶片,但晶片上每個四分之一的部分都包括一個主處理器和64個計算處理器。因此,它基本上是異構的,程式設計起來更像是CPU-GPU混合體,而不是像多核處理器。
為了充分利用異構的GPU加速節點,程式需要具有很強的並行性,而且是型別合適的並行性。這些高度並行處理器不是通過更快的時鐘提供效能,不是由於某種異常奇特的架構。確切地說,那是由於更多的可用門用於並行核心,而這些並行核心用於快取、亂序執行或分支預測。商用CPU的所有那些主要任務被排除在GPU之外,結果是大規模並行處理器在合適的核心和應用程式上提供更高的吞吐量。
這讓我們回到了三個P,而效能不是HPC開發人員的唯一目標。程式設計師需要高效能、良好的生產力和廣泛的可移植性。大多數程式設計師希望程式只編寫一次,而不是多次。
在當時只有節點上的單處理器、所有並行性橫跨節點的時期,程式設計師可以使用扁平的MPI來應付。這類程式可在一系列廣泛的機器上順暢執行,但這不再是我們現在所置身的HPC環境。我們在一個節點中有多個處理器,有SIMD指令,還有異構加速器,引入了更多型別的並行性。為了獲得可移植性,我們需要能夠編寫針對不同數量的核心、不同的SIMD或向量長度可以高效對映的程式,以及同構或異構的系統,沒必要每次改用新系統就要重寫程式。為了確保生產力,我們需要把其中儘可能多的部分抽取出來。我們使用編譯器為今天的HPC系統試圖做到的是與IBM在六十年前使用第一個Fortran編譯器做到同樣的抽象質量,後者實際上是第一種任何型別的高階語言編譯器。
目標是在如今極其複雜的硬體上獲得儘可能接近手動程式設計的效能。今天,就小小的簡單程式碼塊而言,針對英偉達GPU的PGI OpenACC編譯器常常可以提供接近原生CUDA的效能。對於像大型函式這種更復雜的程式碼序列,或者整批呼叫樹被移植到GPU時,接近原生程式碼效能要困難得多。
客觀地說,OpenACC編譯器天生處於劣勢。在CUDA中重寫程式碼時,程式設計師可能查明某個資料結構不適合GPU,可能改變資料結構以增強並行性或效能。也許程式設計師查明程式邏輯的一部分不是很適合GPU,或者查明資料訪問模式並不理想,因此重寫該邏輯或迴圈以改進資料訪問模式。如果你在對應的OpenACC程式中進行同樣這些更改,常常也會獲得好得多的效能。但是你最好不這麼做,如果重寫減慢了多核CPU上程式碼的執行速度,更是如此。因此,OpenACC程式碼可能無法獲得與你花了這番程式設計工作量同樣的效能級別。即便如此,如果OpenACC程式碼的效能足夠接近GPU上的CUDA,基於指令的程式設計模型在生產力和可移植性方面的好處常常很明顯。
儘管過去的60年間我們在編譯器技術方面取得了諸多進展,但一些人仍然認為編譯器與其說是解決辦法,還不如說是問題。他們想要的是來自編譯器的可預測性,而不是在後臺優化編譯器的高階分析和程式碼轉換。這可能引出了這條道路:我們嘗試從編譯器中獲取功能,將更多的責任交到程式設計師的手裡。有人會說,這正是OpenMP今天所走的道路;我看到的危險是,我們到頭來可能為了可預測性而犧牲了可移植性和生產力。比如說,設想你仍然得使用英特爾的SSE和AVX內部函式,對每個迴圈進行手動向量化,而不是針對英特爾至強處理器上的SIMD指令進行自動向量化。你實際上在編寫內聯彙編程式碼。這具有很強的可預測性,但很少有程式設計師想要在該層面編寫所有的計算密集型程式碼,你要為每一代SIMD指令重寫程式碼,或者在非X86 CPU上使用SIMD指令時更是如此。
計算機架構方面任何合理的進步都伴隨編譯器技術方面合理的進步。不能僅用那些架構和編譯器所提供的效能來衡量好處,還要用程式從一代HPC系統移植到下一代(不必完全重寫)後,節省的人力和提高的生產力來衡量。
本文由機器之心經授權轉載自 雲頭條(ID:YunTouTiao ) ,未經授權禁止二次轉載。