天天看點

NI LabVIEW 編譯器:深層分析

注:本文轉自NI網站

概覽

即便對無足輕重的程式設計語言來說,編譯器的設計往往也是一個複雜的課題。即使對專業的軟體工程師們來說,編譯理論也需要考慮專業知識。現代的NI LabVIEW軟體是一種多範例語言,包括廣泛的多種類型的概念,包括資料流,支援面向對象,以及事件驅動程式設計。LabVIEW也覆寫了各種平台,服務多種作業系統(Windows, Linux, Mac),多種晶片組(PowerPC, Intel),甚至可以服務于嵌入式裝置和現場可程式設計門陣列(FPGAs),它與傳統的PC結構有明顯不同。也許您會猜想,LabVIEW編譯器是一個精密的系統,遠遠超出一般書面描述的範圍。

目錄

  1. 編譯與解釋
  2. LabVIEW編譯器的曆史回顧
  3. 當今的編譯器
  4. DFIR 與LLVM協同工作

本專業論文介紹了LabVIEW編譯器,簡明地講解了它從1986年LabVIEW 1.0版本開始的發展變化,并描述了它今天的形式。另外,本文也探究了最近編譯器的創新,并突出了這些新特點對LabVIEW的益處以及對您的幫助。

編譯與解釋

LabVIEW是一種編譯語言,它令人驚奇,因為它在一般的G開發過程沒有任何明晰的編譯步驟。取而代之的是,您可以對您的VI做出改動并簡單地按下運作鍵來執行它。編譯意味着您所寫的G代碼被轉化為本地機器碼然後被主機電腦直接執行。這種方法可供選擇的另一種途徑是解釋,程式被另外的軟體程式(叫做解釋程式)間接地執行,而不是直接由電腦執行。

LabVIEW語言并沒有要求其本身被編譯或者解釋;事實上,LabVIEW的第一個版本使用解釋程式。在後來的版本,編譯器取代了解釋程式以提高VI運作時的性能,這是編譯器與解釋程式相比最常見的差別。解釋程式更容易編寫,并以較差的運作性能為代價,而編譯器執行起來更加複雜但是卻能提供更快的執行時間。LabVIEW編譯器最主要的好處之一就是編譯器對所有VIs的提高顯而易見,而不必做出任何改變。實際上,LabVIEW 2010正式版的編譯器進行了内部優化以加快VI的執行時間。

LabVIEW編譯器的曆史回顧

在急于深入讨論現在編譯器内部組成之前,很值得總結一下編譯器從20年前最早期版本到現在的發展。這裡介紹的一些算法,例如類型傳播法,聚叢法,以及内嵌法(inplaceness),在現代的LabVIEW編譯器讨論中會更加詳細地描述。

LabVIEW 1.0版本于1986上市。如前面提到的,LabVIEW在其第一個版本使用了解釋程式并且僅為摩托羅拉68000服務。那時的LabVIEW語言非常簡單,也減弱了其對編譯器的需求(當時是解釋程式)。例如,它不存在任何的多态資料類型,唯一的資料類型是擴充精度浮點資料。LabVIEW 1.1版本首次引入了内嵌(inplaceness)算法,或者稱之為“内嵌程式”。此算法支援資料配置設定,是以您可以在執行的時候重新使用資料,避免了不必要的資料副本,相應地,常常能顯著地提高執行性能。

在LabVIEW 2.0版本,解釋程式被目前的編譯器所取代。仍然專門為摩托羅拉68000服務,LabVIEW可以生成本地機器碼。在2.0版本還增加了類傳播算法,可以在不斷完善的LabVIEW語言中發揮其它職能,處理文法檢查與類型解析。LabVIEW 2.0另外一項重大的創新是聚叢程式的引入。聚叢算法支援LabVIEW圖表的并行輸入,并将節點歸類為“叢”,它可以并行運作。類傳播算法,嵌入法(inplaceness)以及聚叢算法直到目前也是現代LabVIEW編譯器的重要組成部分,并随時間的推移顯現出更多的新增改進。LabVIEW 2.5中新的編譯器基本結構增加了對多種後端裝置的支援,尤其是英特爾x86與 Sparc。LabVIEW 2.5也引入了連接配接器,當VIs需要被重新編譯的時候,它可以管理VIs路徑之間的從屬關系。

在LabVIEW 3.1,除常數合并外,也增加了兩個新的後端,PowerPC 與 HP PA-RISC。LabVIEW 5.0 與6.0 更新了編碼生成程式,并增加了GenAPI,一種與多種後端連接配接的常用接口。GenAPI交叉編譯對實時開發來說,是非常重要的。實時開發者一般在主機PC上編寫VIs,而将其部署到(将它們編譯到)實時對象。另外,一種循環不變代碼移出的有限形式也包含在内。最終,LabVIEW多任務執行系統被擴充到支援多線程。

LabVIEW 8.0建立的基于5.0版本引入的GenAPI基本結構,新增了寄存器配置設定算法。在GenAPI引入之前,每個節點的生成碼都是由寄存器硬體編碼的。不可執行編碼的有限形式以及死碼删除也被引入。LabVIEW 2009具有64 位 LabVIEW與資料流中間表示(DFIR)。DFIR立即被用于建立更先進形式的循環不變代碼移出,常數合并,死碼删除以及不可執行編碼删除。2009新語言的特點,例如并行循環,都是基于DFIR建立。

最終,在LabVIEW 2010,DFIR提供了新的編譯器優化,例如代數重組,公共子表達式消除,循環展開,以及subVI直接插入。此正式版本也包括在LabVIEW編譯器鍊中采用了低階虛拟機(LLVM)。LLVM是一種開放源代碼的編譯器基本結構,廣泛應用于工業生産。使用LLVM,新增了很多優化,例如指令排程,循環外提,指令組合,條件傳播,以及一種更精密的寄存器配置設定程式。

當今的編譯器

當對LabVIEW編譯器的曆史有了基本了解後,您現在可以探索現代LabVIEW的編譯器了。首先,回顧進階的多種類型編譯步驟概述,然後更加詳細地浏覽每一部分。

一個VI編譯的第一步是類傳播算法。這複雜的一步是為了解析适于終端輸入的隐含類型,并檢測文法錯誤。在G程式設計語言所有可能的文法錯誤都在類傳播算法這一步被檢測。如果算法确定VI有效,編譯繼續。

在類傳播後,VI首先被從結構圖編輯器使用的模型轉化為編譯器使用的DFIR。一旦轉化為DFIR,編譯器對DFIR圖執行幾個變換,分解它 ,優化它,并使其為生成代碼做好準備。很多編譯器的優化——例如,内嵌程式(inplacer)與叢聚程式——被執行轉化并在本步運作。

在DFIR圖示被優化與簡化後,它被翻譯成LLVM中間表示。對LLVM一些列的掃描被執行,通過中間表示來進一步優化并降低其階次,最終變為機器碼。

類傳播

如先前提到的,類傳播算法解析類型并檢測程式錯誤。實際上,此算法包括如下幾個方面的功能:

  • 解析隐藏類型使其适于終端輸入
  • 解析subVI調用并确定其合法性
  • 計算縱向
  • 檢驗VI的周期
  • 檢測并報告文法錯誤

此算法在您對VI進行每個改動後運作,以确定VI是否仍然完好,是以,這步是否是“編譯”的真正部分還存在少許争議。無論如何,它是LabVIEW編譯鍊的一環,非常明顯地相當于傳統編譯器的詞法分析,句法分析,或者是語義分析。

一個适于終端輸入的簡單例子是LabVIEW加法基元。如果您将兩個整數相加,結果是整數,但是如果您将兩個浮點數相加,結果是一個浮點數。類似的案例出現在符合類型的資料,例如陣列和簇。存在其它語言結構,例如對移位寄存器來說,有更複雜的輸入規則。在加法基元的情況下,輸出類型取決于輸入類型,類型被叫做通過圖表“傳播”,這也是算法名字的由來。

這個加法基元的例子也表明類傳播算法的文法檢查職能。假設您連接配接一個整數和一個字元串到一個加法基元——會發生什麼?在這種情況下,将二者的值相加沒有意義,是以類傳播算法将其報告為一個錯誤并将VI标記為“壞的”,它會引起運作箭頭中斷。

中間表示——是什麼與為什麼

在類傳播确定VI是有效的,編譯器繼續并将VI轉化成DFIR。一般來說在詳細設計DFIR之前要考慮中間表示(IRs)。

IR是由編譯過程通過多階段編輯過的使用者程式的表示。IR的概念常見于現代編譯文獻并能應用于任何程式設計語言。

請考慮一些例子。當今有多種流行的IRs。兩種常見的例子是抽象文法樹(AST)與三位址碼。

NI LabVIEW 編譯器:深層分析

t0 <- y

t1 <- 3

t2 <- t0 * t1

t3 <- x

t4 <- t3 + t2

圖 1. AST IR執行個體 表 1.三位址碼 IR執行個體

圖 1顯示了“x + y * 3”表達的 AST表示,而表1 顯示了三位址碼表示

兩種表示方式之間最明顯的一處不同是AST是更進階的。它更類似于程式(C)的源表示而不是對象表示(機器碼)。三位址碼相比之下,是低級的并且更類似于彙編。

不論進階或低級表示都有各自的優點。例如,文法分析,比如可靠性分析,對類似于AST的進階表示比類似于三位址碼的低級表示更容易實作。其它的優化,例如寄存器配置設定或指令排程,一般用低級表示,比如三位址碼來執行。

因為不同的IR有不同的優勢和劣勢,是以很多編譯器(包括 LabVIEW)會使用多種IR。在LabVIEW中,DFIR作為進階IR使用,而LLVM IR作為低級IR使用。

DFIR

在LabVIEW中,作為進階表示的是 DFIR,它是分等級且基于圖形的,其本身類似于G代碼。如同G代碼,DFIR也是由很多包含接線端的節點組成。每個接線端可以連接配接到其它接線端。一些節點,例如包含圖表的循環,也可以相應地包含其它節點。

NI LabVIEW 編譯器:深層分析
NI LabVIEW 編譯器:深層分析

圖 2. LabVIEW G 代碼與相應的DFIR 圖表

圖2顯示了一個簡單的VI以及它的初步DFIR表示。當首次建立一個VI的DFIR圖表時,它是G代碼的直接翻譯,DFIR圖表的節點一般與G代碼中的其它節點進行一對一的通信。随着編譯的進行,DFIR節點有可能被移動或者分開,或者新的DFIR節點被加入。DFIR一個最關鍵的優勢是它保留了G代碼的固有特性,如并行機制等。用三位址碼表示的并行機制相比之下更難識别。

DFIR為LabVIEW編譯器提供了兩個顯著的優勢。首先,DFIR從VI編譯器的表示分離出編輯器。其次,DFIR能用作擁有多個前端和後端的編譯器的公共端。以下是每一個優勢的詳細解讀。

 DFIR圖表從編譯器表示分離出編輯器

在DFIR出現之前,LabVIEW有一個單獨的VI表示,由編輯器和編譯器共享。這樣阻止了編譯器在編譯過程中修改表示,這樣一來,進行編譯器優化就變得困難了。

NI LabVIEW 編譯器:深層分析

圖 3. DFIR 提供一種構架,允許編譯過程中優化您的代碼

圖3顯示了對應于剛才提到的VI的DFIR圖表。此圖表描述了編譯器過程較靠後的部分,此時它已被幾個變換分解并優化過。您可以看到,這個圖表與之前的圖表看起來有很大的不同。例如:

  • 分解變換已經移走了控制,訓示,以及子VI節點,而用新的節點替代它們——UIAccessor, UIUpdater, FunctionResolver和 FunctionCall。
  • 循環不變式代碼已從循環内将增量和乘法節點移出。
  • 聚叢法在For 循環内部增加了YieldIfNeeded節點,可以使執行線程與其它競争的工作項目共享執行。

我們将會在後面的章節對變換進行更深入探讨。

DFIR IR可以作為多個編譯器前端與後端的公共端

LabVIEW可以在數個不同的終端上工作,而其中一些終端與其它終端差别很大,例如,一台x86 台式 PC與一個 Xilinx FPGA。同樣地,LabVIEW為使用者提供了多種計算模型。除了使用G語言的圖形化程式設計,LabVIEW也在提供了例如MathScript的基于文本的數學運算。這就帶來了前端與後端的集中,它們都需要在LabVIEW編譯器下工作。使用DFIR作為公共IR,前端進行生産而後端進行消費,這樣便促進了不同組合之間的重新使用。例如,運作于DFIR圖表的常數合并優化執行過程可以隻需寫入一次而用于台式,實時,FPGA以及嵌入式對象。

DFIR 分解

一旦進入DFIR, VI首先運作一系列的分解變換。分解變換的目标是縮小或标準化DFIR圖表。例如,未連線輸出通道分解會尋找在條件結構和事件結構中沒有被連線并被配置為“Use Default If Unwired”的輸出通道。對這些接線端來說,變換賦給一個常量以預設值,并将其連接配接到接線端,因而使DFIR圖表的“Use Default If Unwired”行為明确。随後的編譯器掃描會完全相同地處理這些接線端并假設它們都有連線的輸入。在這種情況下,語言的“Use Default If Unwired”特征在将表示縮小到更基本的形式後便被“編譯掉”了。

這種做法也可以用于更為複雜的語言特性。例如,分解變換被用于将回報節點縮小到While循環上的移位寄存器中。另外一個分解将并行的For循環分解為幾個順序的具有額外邏輯的For循環,用以為順序循環将輸入分解為可平行化的部分,随後将所有的部分再次組合到一起。

LabVIEW 2010的一個新特征,子VI直接插入,也是作為DFIR分解來執行。在編譯的這個階段,被标記為“直接插入”的子VI的DFIR圖表直接加入調用程式的DFIR圖表。除了避免子VI調用的架空,直接插入法通過将調用與被調程式結合到一個單獨的DFIR圖表,為額外的優化提供了可能性。例如,考慮一個從vi.lib.調用TrimWhitespace.vi的簡單VI。

NI LabVIEW 編譯器:深層分析

圖 4. 用于示範 DFIR優化的簡單VI執行個體

TrimWhitespace.vi在vi.lib中定義如下:

NI LabVIEW 編譯器:深層分析

圖 5.TrimWhitespace.vi 結構圖

子VI直接插入調用程式中,得出等價于如下G代碼的DFIR圖表。

NI LabVIEW 編譯器:深層分析

圖 6. 直接插入的TrimWhitespace.vi DFIR圖表的等價 G代碼

既然子VI圖表直接插入調用程式的圖表,不可擷取代碼的删除和死碼删除能夠簡化代碼。第一個條件結構總是執行,而第二個條件結構從不執行。

NI LabVIEW 編譯器:深層分析

圖 7. 由于輸入邏輯是常量,條件結構可以删除

類似地,循環不變代碼将比對類型的基元移出循環。最終的DFIR圖表等價于如下的G代碼。

NI LabVIEW 編譯器:深層分析

圖 8. 最終 DFIR圖表的等價G代碼

因為TrimWhitespace.vi在LabVIEW 2010 版本中預設标定為直接插入,所有此VI的用戶端使用都能自動享有這些益處。

DFIR 優化

在DFIR圖表完全地分解後,DFIR優化掃描開始。更多的優化在随後的LLVM編譯時被執行。本章節僅講述了衆多優化當中的一部分。這些變換都是常用的編譯器優化,是以,想找到更多具體優化的資訊應該較為容易。

無法讀取代碼的删除

無法讀取代碼是永遠無法執行的代碼。删除無法讀取代碼并不直接讓執行變得更快,但是它可以使您的代碼更精簡并且改善編譯時間,因為删除的代碼在随後的編譯掃描中将不再被通路到。

NI LabVIEW 編譯器:深層分析

在無法讀取代碼删除之前

NI LabVIEW 編譯器:深層分析

After Unreachable Code Elimination

圖 9. DFIR無法讀取代碼删除分解的等價G代碼

在這個例子中,條件結構的“Do not increment”圖表從不執行,是以變換删除了這個條件。由于條件結構隻剩下一個條件分支,是以它被順序結構替換。随後的死碼删除移除了邊框與枚舉常量。

循環不變代碼移動

循環不變代碼移動将識别循環内部可以安全移至外部的代碼。由于移出代碼的執行次數更少,整體執行速度将得到改善。

NI LabVIEW 編譯器:深層分析
循環不變代碼移動變換之前
NI LabVIEW 編譯器:深層分析
循環不變代碼移動變換之後

圖 10. DFIR循環不變代碼移出分解的等價G代碼

在這個例子中,增量運算被移到循環外面。循環本體不變,是以在建立數組的同時,不必在每個疊代重複進行計算。

公共子表達式删除

公共子表達式删除可識别重複計算,而将執行一次計算,并重複使用計算結果。

NI LabVIEW 編譯器:深層分析

                    Before                                                               

NI LabVIEW 編譯器:深層分析

   After                  

圖 11.  DFIR 公共子表達式删除分解的等價G代碼

常數合并

常數合并支援那些在運作時是常數的圖表部分,因而可以在初期就确定下來

NI LabVIEW 編譯器:深層分析

圖 12. 常數合并在 LabVIEW結構圖中非常直覺

圖12中VI的哈希碼指出了常數合并的一部分。在這種情況下,“偏量”控制不能夠常數合并,而加法基元的其它操作數,包括For循環,是恒定值。

循環展開

循環展開通過在合成碼部分多次重複一個循環的本體以及減少相同因子總的疊代計數,減少了循環架空。這樣減少了循環架空,并且在代碼尺寸增長損失的情況下,顯露了更多的優化過程。

死碼删除

死碼是多餘的代碼。去除死碼加快了執行時間,因為去除的死碼不再被執行。

死碼并不是由您直接編寫的,它常常由DFIR圖表轉換操作産生。請考慮如下的例子。無法讀取代碼的删除确定事件結構可以被移除。這樣“建立”的死碼可以被死碼删除轉換移走。

NI LabVIEW 編譯器:深層分析

先前

NI LabVIEW 編譯器:深層分析

在無法讀取代碼删除後

NI LabVIEW 編譯器:深層分析

 在死碼删除後

圖13. 死碼删除能夠減少編譯器需要跨越的代碼數量

此小節涉及的大部分轉換具有像這樣的互相關系;運作一個轉換也許會引發其它轉換運作的機會

DFIR後端轉換

在DFIR圖表被分解并優化後,很多後端轉換被執行。這些轉換評估并注解DFIR,為最終将DFIR圖表降低為LLVM IR做好準備

聚叢程式

聚叢算法分析DFIR圖表的并行機制,并将節點歸類為您可以并行運作的叢。這種算法與LabVIEW實時執行系統緊密聯系,這些系統使用多線程協同多任務處理。每個由聚叢程式産生的叢都作為執行系統的單獨任務羅列出來。叢中的節點以固定的,串行化的次序執行。每個叢具有預訂的執行次序允許替代程式共享資料配置設定并顯著地提高了性能。聚叢程式也具有将結果插入長操作的職能。例如循環或者I/O,是以這些聚叢程式與其它聚叢程式協同執行多任務處理。

内嵌程式

内嵌程式分析DFIR圖并識别什麼時候您可以重新使用資料配置設定以及什麼時候您必須進行複制。LabVIEW中的一個連接配接也許是一個簡單的32位标量或32 MB的陣列。確定資料盡可能地重複使用對LabVIEW這樣的資料流語言來說是至關重要的。

請考慮如下的例子(請注意VI調試不能實作最好的性能和存儲器空間占用)

NI LabVIEW 編譯器:深層分析

圖 14. 簡單執行個體示範了内嵌算法

這個VI初始化一個陣列,對每個要素增加了一些标量值,并将其編寫為一個二進制檔案。應該有多少個陣列副本? LabVIEW最初不得不在本地建立陣列,而加法運算隻能在那個陣列運作。是以隻需要一個陣列的副本而不是每個連接配接都配置設定。這意味着一個顯著的不同——無論是存儲器消耗還是執行時間——如果陣列很大。在這個VI,内嵌程式意識到運作“内嵌”的時機并配置加法節點以利用它。

您可以在Tools»Profile下使用“緩沖區配置設定”來檢驗您編寫的VIs的這種行為。工具不會顯示加法基元的配置設定,而顯示為沒有資料副本并且加法運算内嵌。

這是可以接受的,因為沒有其它節點需要原始陣列。如果您如圖15所示修改了VI,内嵌程式會為加法基元制作一個副本。這是因為第二次寫為二進制(Write to Binary File)需要原始的陣列并且必須在第一次寫為二進制基元(Write to Binary File Primitive)後。采取此修改,顯示緩沖區配置設定工具顯示加法基元的配置設定。

NI LabVIEW 編譯器:深層分析

圖 15. 原始陣列連線分支引起存儲器中副本的建立

配置設定程式

在内嵌程式識别出哪一個節點可以與其它節點共享存儲單元,配置設定程式運作以建立VI需要執行的配置設定區。它通過通路每一個節點與端點來執行。内嵌到其它端點的端點重新使用配置設定區而不必建立一個新的。

編碼發生程式

編碼發生程式是編譯器的組成部分,為對象過程将DFIR圖轉化為可執行的機器指令。LabVIEW在DFIR圖按資料流次序渡越每個節點,每個節點調用一個叫做GenAPI的接口,它被用來将DFIR圖轉化為順序的中間語言(IL),以描述節點的功能性。IL提供了一個獨立平台來描述節點的低級行為。IL的多種指令被用來執行運算,讀寫存儲器,實作比較與條件跳轉,等等。IL指令能夠對存儲器或用來存儲中間值的虛拟寄存器中的值進行操作。常用IL指令包括GenAdd, GenMul, GenIf, GenLabel,及 GenMove。

在LabVIEW 2009及早期版本中,IL結構直接轉化為用于對象平台的機器指令(例如 80X86與 PowerPC)。LabVIEW使用一個簡單的一次掃描寄存器配置設定程式将虛拟寄存器映射到實體機器寄存器。每個IL指令發出一組用于特定機器指令的硬體編碼,進而在每個支援對象的平台執行它。它盲目地追求速度,是一種即席(ad hoc)網絡,産生品質低下的代碼,并不适于優化。DFIR,作為進階的,獨立平台表示,受限于它能支援的代碼轉換類型。在現代優化編譯器中,為增加支援整套代碼優化,LabVIEW近來采用一種第三方開放源碼技術,稱為LLVM。

LLVM

低級虛拟機(LLVM)是一種多用途,高性能,開放源代碼的編譯器構架,起初作為伊利諾斯州立大學的一個研究項目被發明出來。LLVM 現在廣泛地用于學術和工業,因為它靈活,簡潔的API以及無許可證限制。

在LabVIEW 2010版本,LabVIEW 編碼生成程式是使用LLVM重構生成對象機器代碼。已經存在的LabVIEW IL表示為這種嘗試提供了很好的起點,隻需要重寫大概80 條IL指令,不超過LabVIEW支援的大量DFIR節點與基元。

在由VI的DFIR圖建立IL代碼流後,LabVIEW通路每個IL指令并建立一個等價的LLVM彙編表示。它可以援引多種優化掃描,然後使用LLVM即時(JIT)架構進而在存儲器中建立可執行機器指令。LLVM的機器變換布置資訊被轉化為LabVIEW表示,是以當您将VI儲存到硬碟中并在另外不同的基于位址的存儲器中重新載入時,您可以正确地修改使其在新的位址運作。

LabVIEW使用LLVM實作的一些标準編譯器優化包括如下:

  • 指令綜合
  • 跳轉線程
  • 聚合體标量替換
  • 條件傳播
  • 尾調用消除
  • 表達重組
  • 循環不變代碼移出
  • 循環外提與引導分割
  • 歸納變量簡化
  • 循環展開
  • 總值編号
  • 死存儲消除
  • 主動死代碼删除
  • 稀有條件持續傳播

所有這些優化的完整解釋超出了本文的範圍,而網絡上以及大部分的編譯器教科書都有關于它們的豐富資訊。

内部基準顯示LLVM的引入節約了20%的VI執行時間。單獨的結果取決于VI執行運算的類型;一些VIs的提高遠勝于此,一些性能并無變化。例如,使用先進分析庫的VIs 或者其它的很大程度依賴于代碼的,如使用優化過的C實作的VIs,性能看起來幾乎沒有不同。LabVIEW 2010是使用LLVM的首個版本,仍然具有很多有待挖掘的潛力用于未來的改進。

DFIR 與LLVM協同工作

您也許已經注意到了,這些優化當中的某一些,比如循環不變代碼移出以及死碼删除,已經由DFIR正在執行。事實上,某些優化掃描适于多次,并在編譯器的不同層次運作,因為其它的優化掃描或許在将代碼轉換時,出現有用的新的優化機會。最終标準是,DFIR作為進階IR,LLVM作為低級IR,它們兩個協同優化您為處理器結構所寫的,用于代碼執行的LabVIEW代碼。

繼續閱讀