Flambda 是指 OCaml 4.03 版之後,原生碼編譯器提供的一系列最佳化過程的術語。
Flambda 旨在讓使用者更容易編寫慣用的 OCaml 程式碼,而不會產生效能損失。
若要使用 Flambda 最佳化器,必須將 -flambda 選項傳遞給 OCaml 的 configure 指令稿。(沒有支援單一編譯器可以在 Flambda 和非 Flambda 模式下運作。)使用 Flambda 編譯的程式碼不能與未使用 Flambda 編譯的程式碼連結到同一個程式中。嘗試這樣做會導致編譯器錯誤。
可以使用 -config 選項呼叫特定的 ocamlopt 並尋找任何以「flambda:」開頭的行,來判斷其是否使用 Flambda。如果存在這樣的行,且表示「true」,則表示支援 Flambda,否則不支援。
Flambda 跨越不同的編譯單元提供完整的最佳化,前提是目前正在編譯的單元的相依性的 .cmx 檔案可用。(編譯單元對應於單個 .ml 原始碼檔案。)但是,它尚未完全作為整個程式的編譯器運作:例如,不支援跨越一整組編譯單元來消除無用程式碼。
產生位元組碼時,目前不支援使用 Flambda 進行最佳化。
一般而言,Flambda 不應影響現有程式的語意。此規則有兩個例外:可能會消除正在進行基準測試的純程式碼(請參閱23.14節),以及使用不安全操作的程式碼的行為變更(請參閱23.15節)。
Flambda 尚未最佳化陣列或字串的邊界檢查。它也不會從使用者在程式碼中寫入的任何斷言中取得最佳化的提示。
請參閱本章結尾的詞彙表,了解下方使用的技術術語的定義。
Flambda 最佳化器提供各種可用於控制其行為的命令列旗標。參考章節中提供了每個旗標的詳細說明。這些章節也說明了特定旗標所採用的任何引數。
常用選項
較少使用的選項
進階選項,僅在詳細調整時需要
Flambda 在輪次中運作:一輪包含某個轉換序列,然後可以重複該序列以獲得更令人滿意的結果。可以使用 -rounds 參數手動設定輪次數(儘管在使用預先定義的最佳化層級時,例如使用 -O2 和 -O3 時,不需要這樣做)。對於高最佳化,輪次數可以設定為 3 或 4。
可以每輪套用的命令列旗標,例如名稱中帶有 -cost 的旗標,接受以下形式的引數
旗標 -Oclassic、-O2 和 -O3 會在所有其他旗標之前套用,這表示可以覆寫某些參數,而無需指定給定最佳化層級通常調用的每個參數。
內聯是指將函式的程式碼複製到呼叫函式的位置。函式的程式碼將被其參數與對應引數的繫結包圍。
內聯的目的是
這些目標的達成,往往不單單是透過內聯本身,也透過編譯器因內聯而能夠執行的其他最佳化。
當遞迴呼叫一個函數(在該函數的定義內,或在同一個相互遞迴群組中的另一個函數內)被內聯時,這個過程也被稱為展開(unrolling)。這有點類似於迴圈剝離(loop peeling)。例如,給定以下程式碼
let rec fact x = if x = 0 then 1 else x * fact (x - 1) let n = fact 4
在呼叫點 fact 4 展開一次會產生(fact 的主體保持不變):
let n = if 4 = 0 then 1 else 4 * fact (4 - 1)
這簡化為:
let n = 4 * fact 3
相較於編譯器的先前版本,Flambda 提供了顯著增強的內聯能力。
內聯與所有其他 Flambda 最佳化過程一起執行,也就是說,在閉包轉換之後。相較於在閉包轉換之前可能更直接的實作方式,這有三個特別的優點:
在 -Oclassic 模式中,Flambda 內聯器的行為模仿了編譯器的先前版本。(程式碼仍然可能會受到編譯器先前版本未執行的進一步最佳化:函子可能會被內聯,常數會被提升,而未使用的程式碼會被消除,所有這些都在本章的其他地方描述。請參閱 23.3.3、23.8.1 和 23.10 各節)。在函數的定義站點,會測量函數的主體。如果滿足以下條件,它將被標記為符合內聯條件(因此會在每個直接呼叫點被內聯):
編譯器的非 Flambda 版本無法內聯包含另一個函數定義的函數。然而,-Oclassic 允許這樣做。此外,非 Flambda 版本也無法內聯僅因先前的內聯過程而公開的函數,但 -Oclassic 再次允許這樣做。例如:
module M : sig val i : int end = struct let f x = let g y = x + y in g let h = f 3 let i = h 4 (* h is correctly discovered to be g and inlined *) end
所有這些都與正常的 Flambda 模式形成對比,也就是說,沒有 -Oclassic,在這種情況下:
Flambda 模式在下一節中描述。
每當編譯器配置為 Flambda 且未指定 -Oclassic 時,都會使用 Flambda 內聯啟發法,在呼叫點做出內聯決策。這有助於在上下文很重要的情況下。例如:
let f b x = if b then x else ... big expression ... let g x = f true x
在這種情況下,我們希望將 f 內聯到 g 中,因為可以消除條件跳轉,程式碼大小應該會減少。如果內聯決策是在宣告 f 後在沒有看到它的使用情況下做出的,那麼它的大小可能使其不符合內聯條件;但在呼叫點,可以知道它的最終大小。此外,這個函數可能不應該系統地內聯:如果 b 是未知的,或者實際上是 false,那麼權衡程式碼大小的巨大增加幾乎沒有好處。在現有的非 Flambda 內聯器中,這不是一個大問題,因為內聯鏈很快就被截斷了。然而,這導致過度使用過大的內聯參數,例如 -inline 10000。
更詳細地說,在每個呼叫點都會執行以下程序:
對同一個相互遞迴群組中其他函數的呼叫在遞迴函數內的內聯,會受到下面描述的展開深度的控制。這確保函數不會被過度展開。(只有在選擇 -O3 最佳化等級和/或傳遞帶有大於零的參數的 -inline-max-unroll 標記時,才會啟用展開。)
與普通函數相比,沒有什麼特別的因素會阻止函子進行內聯。對內聯器來說,它們看起來都一樣,只是函子被標記為這樣。
對頂層函子的應用傾向於內聯。(可以調整此偏差:請參閱下面 -inline-lifting-benefit 的文件。)
不在頂層的函子的應用,例如在某些其他表達式內的局部模組中,內聯器會將它們視為與普通函數呼叫相同。
如果內聯器知道將要呼叫哪個特定函數,它將能夠考慮內聯對一級模組中函數的呼叫。包含模組中一組函數的頂級模組記錄本身不會阻止內聯。
目前,Flambda 不會內聯對物件的方法呼叫。
如果向編譯器提供 -inlining-report 選項,那麼將會發出一個對應於每一輪最佳化的檔案。對於 OCaml 原始檔 *basename*.ml,這些檔案的名稱為 *basename*.*round*.inlining.org,其中 *round* 是一個從零開始的整數。在這些檔案中,它們的格式為「org 模式」,會找到描述內聯器所做決策的英文散文。
內聯通常會導致程式碼大小增加,如果沒有加以控制,不僅可能導致可執行檔過於龐大和編譯時間過長,還可能由於更差的局部性而導致效能下降。因此,Flambda 內聯器會權衡程式碼大小的變化與預期的執行時效能效益,而效益的計算基於編譯器觀察到可能因內聯而移除的操作次數。
例如,給定以下程式碼:
let f b x = if b then x else ... big expression ... let g x = f true x
將觀察到內聯 f 將會移除:
形式上,執行時效能效益的估計是透過首先加總因內聯和隨後簡化內聯主體而已知會移除的操作成本來計算的。各種操作種類的個別成本可以使用各種 -inline-...-cost 標記來調整,如下所示。成本指定為整數。所有這些標記都接受一個單一參數,該參數使用 23.2.1 節中詳述的約定來描述此類整數。
(預設值在下面的 23.5 節中描述。)
然後,初始效益值會按一個因數縮放,該因數嘗試補償程式碼中的當前點(如果處於一定數量的條件分支之下)可能為冷路徑的事實。(Flambda 目前不計算熱路徑和冷路徑。)該因數(估計的內聯器確實處於熱路徑的機率)計算為 1/(1 + f)d,其中 f 由 -inline-branch-factor 設定,d 是目前點的分支巢狀深度。當內聯器下降到更深層的巢狀分支時,內聯的好處就會減少。
產生的效益值稱為估計效益。
程式碼大小的變化也會被估計:嚴格來說,它應該是機器碼大小的變化,但由於內聯器無法使用它,因此會使用近似值。
如果估計的效益超過程式碼大小的增加,那麼將保留該函數的內聯版本。否則,該函數將不會被內聯。
頂層函子的應用將獲得額外的好處(可以通過 -inline-lifting-benefit 標記控制),以便在這種情況下傾向於保持內聯版本。
如上所述,有三個參數會限制在投機期間搜尋內聯機會:
這些參數最終會受到提供給相應命令列標記的參數(或其預設值)的限制:
特別要注意,-inline 不具有其在先前編譯器或 -Oclassic 模式中的含義。在這兩種情況下,-inline 實際上是對內聯效益的一些基本評估。然而,在 Flambda 內聯模式下,它對應於對搜尋的限制;對效益的評估是獨立的,如上所述。
當開始推測時,內聯閾值會從 -inline 設定的值開始(如果適用,則為 -inline-toplevel,請參閱上方)。在做出推測性的內聯決策後,閾值會減少被內聯函數的程式碼大小。如果閾值耗盡,降至零或以下,則不會執行進一步的推測。
內聯深度從零開始,每次內聯器深入另一個函數時都會增加一。每次內聯器離開這樣的函數時,它就會減少一。如果深度超過 -inline-max-depth 設定的值,則推測會停止。此參數旨在作為一種通用後備機制,以應對內聯閾值無法充分控制搜尋的情況。
展開深度適用於同一相互遞迴函數群組內的呼叫。每次執行此類呼叫的內聯時,在檢查產生的主體時,深度會增加一。如果深度達到 -inline-max-unroll 設定的限制,則推測會停止。
內聯器可能會發現對遞迴函數的呼叫點,其中已知關於引數的一些資訊:例如,它們可能等於目前作用域中的其他變數。在這種情況下,將函數特殊化為這些引數可能是有益的。這是透過複製函數的宣告(以及任何其他參與任何相同相互遞迴宣告的函數)並記錄有關引數的額外資訊來完成的。由這些資訊增強的引數稱為特殊化引數。為了盡量確保不會無用地執行特殊化,只有當可以證明引數是不變的時才會將其特殊化:換句話說,在遞迴函數本身執行期間,引數永遠不會改變。
除非被屬性覆蓋(見下文),否則在以下情況下不會嘗試函數的特殊化:
編譯器可以證明遞迴群組內多個函數的函數引數不變性(儘管這有一些限制,如下例所示)。
應該注意的是,閉包拆箱傳遞(見下文)可以在非遞迴函數上引入特殊化引數。(目前編譯器中沒有其他地方這樣做。)
這個函數可能會這樣寫
let rec iter f l = match l with | [] -> () | h :: t -> f h; iter f t
並像這樣使用
let print_int x = print_endline (Int.to_string x) let run xs = iter print_int (List.rev xs)
iter 的引數 f 是不變的,因此可以將該函數特殊化
let run xs = let rec iter' f l = (* The compiler knows: f holds the same value as foo throughout iter'. *) match l with | [] -> () | h :: t -> f h; iter' f t in iter' print_int (List.rev xs)
編譯器記錄下來,對於函數 iter’,引數 f 特殊化為常數閉包 print_int。這表示 iter’ 的主體可以簡化
let run xs = let rec iter' f l = (* The compiler knows: f holds the same value as foo throughout iter'. *) match l with | [] -> () | h :: t -> print_int h; (* this is now a direct call *) iter' f t in iter' print_int (List.rev xs)
對 print_int 的呼叫確實可以內聯
let run xs = let rec iter' f l = (* The compiler knows: f holds the same value as foo throughout iter'. *) match l with | [] -> () | h :: t -> print_endline (Int.to_string h); iter' f t in iter' print_int (List.rev xs)
現在可以移除未使用的特殊化引數 f,留下
let run xs = let rec iter' l = match l with | [] -> () | h :: t -> print_endline (Int.to_string h); iter' t in iter' (List.rev xs)
編譯器目前無法偵測到以下情況下的不變性。
let rec iter_swap f g l = match l with | [] -> () | 0 :: t -> iter_swap g f l | h :: t -> f h; iter_swap f g t
特殊化的效益評估方式與內聯類似。特殊化的引數資訊可能表示可以簡化正在特殊化的函數主體:被移除的操作會累積到效益中。然後,將此與重複(特殊化)函數宣告的大小一起評估,以對比原始函數呼叫的大小。
預設設定(不使用 -Oclassic 時)是使用以下參數進行一輪最佳化。
參數 | 設定 |
-inline | 10 |
-inline-branch-factor | 0.1 |
-inline-alloc-cost | 7 |
-inline-branch-cost | 5 |
-inline-call-cost | 5 |
-inline-indirect-cost | 4 |
-inline-prim-cost | 3 |
-inline-lifting-benefit | 1300 |
-inline-toplevel | 160 |
-inline-max-depth | 1 |
-inline-max-unroll | 0 |
-unbox-closures-factor | 10 |
當指定 -O2 時,會執行兩輪最佳化。第一輪使用預設參數(見上文)。第二輪使用以下參數。
參數 | 設定 |
-inline | 25 |
-inline-branch-factor | 與預設相同 |
-inline-alloc-cost | 預設值的兩倍 |
-inline-branch-cost | 預設值的兩倍 |
-inline-call-cost | 預設值的兩倍 |
-inline-indirect-cost | 預設值的兩倍 |
-inline-prim-cost | 預設值的兩倍 |
-inline-lifting-benefit | 與預設相同 |
-inline-toplevel | 400 |
-inline-max-depth | 2 |
-inline-max-unroll | 與預設相同 |
-unbox-closures-factor | 與預設相同 |
當指定 -O3 時,會執行三輪最佳化。前兩輪與 -O2 相同。第三輪使用以下參數。
參數 | 設定 |
-inline | 50 |
-inline-branch-factor | 與預設相同 |
-inline-alloc-cost | 預設值的三倍 |
-inline-branch-cost | 預設值的三倍 |
-inline-call-cost | 預設值的三倍 |
-inline-indirect-cost | 預設值的三倍 |
-inline-prim-cost | 預設值的三倍 |
-inline-lifting-benefit | 與預設相同 |
-inline-toplevel | 800 |
-inline-max-depth | 3 |
-inline-max-unroll | 1 |
-unbox-closures-factor | 與預設相同 |
如果內聯器被證明難以處理,並且拒絕內聯特定函數,或者如果觀察到的內聯決策因其他原因不令程式設計人員滿意,則可以由程式設計人員直接在原始碼中指示內聯行為。一個可能適合這樣做的情境是,當程式設計人員(而不是編譯器)知道特定函數呼叫位於冷程式碼路徑上時。可能希望防止內聯函數,以便使熱路徑上的程式碼大小保持較小,從而提高區域性。
內聯器使用屬性來導引。對於非遞迴函數(以及遞迴函數的一步展開,儘管 @unroll 對於此目的更清楚),支援以下屬性:
對於遞迴函數,相關屬性為
如果發現無法遵守 @inlined 或 @specialised 屬性的註釋,則會發出編譯器警告。
module F (M : sig type t end) = struct let[@inline never] bar x = x * 3 let foo x = (bar [@inlined]) (42 + x) end [@@inline never] module X = F [@inlined] (struct type t = int end)
與內聯一起執行的簡化會傳播關於哪些變數在執行時保留哪些值的資訊(稱為近似值)。還會追蹤變數和符號之間的某些關係:例如,可能已知某些變數始終保留與其他某些變數相同的值;或者可能已知某些變數始終保留某些符號指向的值。
在以下情況下,傳播可以幫助消除配置:
let f x y = ... let p = x, y in ... ... (fst p) ... (snd p) ...
來自 p 的投影可以替換為對變數 x 和 y 的使用,這可能表示 p 變得未使用。
簡化傳遞執行的傳播對於發現哪些函數流向間接呼叫點也很重要。這可以使此類呼叫點轉換為直接呼叫點,這使其有資格進行內聯轉換。
請注意,即使在 safe-string 模式下,也不會傳播有關字串內容的任何資訊,因為目前無法保證它們在給定程式中是不可變的。
當發現運算式為常數時,它們會被提升到符號繫結,也就是說,它們會靜態配置在物件檔案中,當它們評估為裝箱值時。此類常數可能是簡單的數值常數,例如浮點數 42.0,或更複雜的值,例如常數閉包。
將常數提升到頂層會減少執行時的配置。
編譯器旨在共享提升到頂層的常數,以使沒有重複的定義。但是,如果 .cmx 檔案對編譯器隱藏,則可能無法實現最大程度的共享。
以下語言語義專門適用於常數浮點數陣列。(「常數浮點數陣列」是指完全由編譯時已知的浮點數組成的陣列。常見的情況是文字,例如 [| 42.0; 43.0; |]。)
頂層 let 表達式可能會被提升為符號綁定,以確保對應的綁定變數不會被閉包捕獲。如果發現給定綁定的定義表達式是常數,則會將其綁定為常數(技術術語是let-symbol綁定)。
否則,該符號會綁定到一個(靜態分配的)預先分配的區塊,其中包含一個欄位。在執行時,將會評估定義的表達式,並將區塊的第一個欄位填入產生的值。這種初始化符號綁定會導致額外的一次間接訪問,但由於符號的位址在編譯時已知,因此可以確保值的用法不會被閉包捕獲。
應該注意的是,對應於初始化符號綁定的區塊會永遠保持活動狀態,因為它們出現在物件檔案中 GC 根的靜態表格中。表達式的這種延長的生命週期有時可能會令人驚訝。如果希望建立一些不具有這種延長生命週期的非常數值(例如在編寫 GC 測試時),則可以在函式內部建立並使用它,並且該函式的應用點(可能在頂層) — 或實際上是函式宣告本身 — 會被標記為永不內聯。此技術可防止提升有問題的值的定義(當然,假設它不是常數)。
本節中的轉換與分割已裝箱(也就是說,非立即)的值有關。它們的主要目的是減少分配,這往往會產生具有較低變異數和較小尾部的執行時效能分析。
除非提供 -no-unbox-free-vars-of-closures,否則會啟用此轉換。
出現在閉包環境中的變數本身可能是已裝箱的值。因此,它們可能會被分割成更多的閉包變數,每個變數對應於原始閉包變數的一些投影。此轉換稱為取消裝箱閉包變數或取消裝箱閉包的自由變數。只有在合理確定對應的函式主體內沒有使用已裝箱的自由變數本身時,才會應用此轉換。
在以下程式碼中,編譯器觀察到從函式 f 返回的閉包包含一個變數 pair(在 f 的主體中是自由的),該變數可以分割為兩個獨立的變數。
let f x0 x1 = let pair = x0, x1 in Printf.printf "foo\n"; fun y -> fst pair + snd pair + y
經過一些簡化後,得到
let f x0 x1 = let pair_0 = x0 in let pair_1 = x1 in Printf.printf "foo\n"; fun y -> pair_0 + pair_1 + y
然後得到
let f x0 x1 = Printf.printf "foo\n"; fun y -> x0 + x1 + y
已消除 pair 的分配。
如果此轉換會導致閉包包含的閉包變數是之前的兩倍以上,則此轉換不會執行。
除非提供 -no-unbox-specialised-args,否則會啟用此轉換。
在編譯過程中,函式的一個或多個不變引數可能會特化為特定值。當這些值本身被裝箱時,對應的特化引數可能會被分割成更多特化引數,這些引數對應於函式主體中出現的已裝箱值的投影。此轉換稱為取消裝箱特化引數。只有在合理確定函式內未使用已裝箱的引數本身時,才會應用此轉換。
如果所討論的函式參與遞迴群組,則可以基於不變引數之間的資料流,立即在群組中複製取消裝箱特化引數。
在給定以下程式碼後,編譯器會將 loop 內聯到 f 中,然後觀察到 inv 是不變的,並且始終是由將 42 和 43 新增至函式 f 的引數 x 所形成的 pair。
let rec loop inv xs = match xs with | [] -> fst inv + snd inv | x::xs -> x + loop2 xs inv and loop2 ys inv = match ys with | [] -> 4 | y::ys -> y - loop inv ys let f x = Printf.printf "%d\n" (loop (x + 42, x + 43) [1; 2; 3])
由於函式的引數足夠少,因此將會新增更多特化引數。經過一些簡化後,得到
let f x = let rec loop' xs inv_0 inv_1 = match xs with | [] -> inv_0 + inv_1 | x::xs -> x + loop2' xs inv_0 inv_1 and loop2' ys inv_0 inv_1 = match ys with | [] -> 4 | y::ys -> y - loop' ys inv_0 inv_1 in Printf.printf "%d\n" (loop' [1; 2; 3] (x + 42) (x + 43))
已移除 f 內 pair 的分配。(由於 loop' 和 loop2' 的兩個閉包是常數,它們也將被提升到頂層,而不會產生執行時分配的損失。即使未執行取消裝箱特化引數的轉換,也會發生這種情況。)
取消裝箱特化引數的轉換永遠不會引入額外的分配。
如果取消裝箱引數會導致原始函式具有足夠多的引數,從而抑制尾呼叫最佳化,則此轉換不會取消裝箱引數。
此轉換的實作方式是建立一個接受原始引數的包裝函式。同時,原始函式會被重新命名,並且會新增額外引數,這些引數對應於取消裝箱的特化引數;此新函式會從包裝函式呼叫。然後,包裝函式將在直接呼叫站點內聯。事實上,除非正在使用 -unbox-closures,否則所有呼叫站點都將是直接的,因為它們最初在特化函式時是由編譯器產生的。(在 -unbox-closures 的情況下,其他函式可能會出現特化引數;在這種情況下,可能會出現間接呼叫,並且由於必須透過包裝函式彈回,這些呼叫會產生少量損失。用於 -unbox-closures 的直接呼叫代理技術不會被取消裝箱特化引數的轉換使用。)
預設不會啟用此轉換。可以使用 -unbox-closures 旗標啟用此轉換。
此轉換會將閉包變數取代為特化引數。目的是使更多閉包變成封閉的。當所涉及的函式無法內聯或特化時,它作為減少分配的一種手段特別適用。例如,某些非遞迴函式可能太大而無法內聯;或者某些遞迴函式可能沒有提供特化的機會,可能是因為其唯一引數是 unit 類型。
目前,啟用此轉換時,實際執行時效能可能會略有損失,但是由於分配減少,可能會獲得更穩定的效能。建議開發人員進行實驗以確定該選項是否對其程式碼有利。(預計將來可以消除效能下降的情況。)
在以下程式碼中(當 g 太大而無法內聯時通常會發生這種情況),x 的值通常會透過 g 的閉包傳遞到 + 函式的應用程式。
let f x = let g y = x + y in (g [@inlined never]) 42
取消裝箱閉包會導致 g 內 x 的值作為引數傳遞給 g,而不是透過其閉包傳遞。這表示 g 的閉包變成常數,並且可以提升到頂層,從而消除執行時分配。
此轉換的實作方式是,以取消裝箱特化引數時所用的方式新增一個新的包裝函式。閉包變數在包裝函式中仍然是自由的,但目的是當包裝函式在直接呼叫站點內聯時,相關的值會透過新的特化引數直接傳遞給主函式。
新增這樣的包裝函式會懲罰對函式的間接呼叫(這些呼叫可能會存在於任意位置;請記住,例如,此轉換並非僅應用於編譯器由於特化而產生的函式),因為此類呼叫會透過包裝函式彈回。為了減輕這種情況,如果函式的大小相對於要移除的自由變數數量來說足夠小,則此轉換將複製該函式以獲得兩個版本:原始版本(用於間接呼叫,因為我們無法做得更好)和前面段落中所述的包裝函式/重寫函式對。包裝函式/重寫函式對將僅用於函式的直接呼叫站點。(在這種情況下,包裝函式稱為直接呼叫代理,因為它在直接呼叫站點取代了另一個函式 — 用於間接呼叫的未變更版本。)
可以使用 -unbox-closures-factor 命令列旗標(該旗標接受一個整數)來調整將函式視為大到無法複製的點。複製的優點會按整數縮放,然後再針對大小進行評估。
在以下程式碼中,有兩個閉包變數通常會導致閉包分配。一個稱為 fv,出現在函式 baz 內部;另一個稱為 z,出現在函式 bar 內部。在這個玩具(但複雜)的範例中,我們再次使用屬性來模擬 baz 的第一個引數太大而無法內聯的典型情況。
let foo c = let rec bar zs fv = match zs with | [] -> [] | z::zs -> let rec baz f = function | [] -> [] | a::l -> let r = fv + ((f [@inlined never]) a) in r :: baz f l in (map2 (fun y -> z + y) [z; 2; 3; 4]) @ bar zs fv in Printf.printf "%d" (List.length (bar [1; 2; 3; 4] c))
將 -O3 -unbox-closures 套用到此程式碼所產生的程式碼會透過函式引數傳遞自由變數,以便在此範例中消除所有閉包分配(除了可能在 printf 內部執行的任何分配)。
只要簡化傳遞移除未使用的 let 綁定,並且其對應的定義表達式具有「無效應」。請參閱下面的「效果處理」一節,以了解此術語的精確定義。
此轉換與移除定義表達式無效應的 let 表達式類似。它改為對符號綁定進行運作,移除那些無效應的符號綁定。
預設僅針對特化引數啟用此轉換。可以使用 -remove-unused-arguments 旗標為所有引數啟用此轉換。
此步驟會分析函式,以判斷哪些引數未使用。移除的方式是建立一個包裝函式,該函式將在每個直接呼叫點內聯,接受原始引數,然後在呼叫原始函式之前捨棄未使用的引數。因此,如果原始函式通常是間接呼叫的,則此轉換可能會不利,因為此類呼叫現在會透過包裝函式傳遞。(用於在取消封裝閉包變數期間減少這種懲罰的直接呼叫替代技術 (如上所述) 尚未應用於移除未使用引數的步驟。)
此轉換會在整個編譯單元中執行分析,以判斷是否存在從未使用過的閉包變數。然後會消除這些閉包變數。(請注意,這必須是全單元分析,因為由於內聯,從特定閉包投影出的閉包變數可能已傳播到程式碼中的任意位置。)
Flambda 執行一個簡單的分析,類似於編譯器中其他地方執行的分析,該分析可以將 ref 轉換為可變變數,然後這些變數可以保存在暫存器中 (或根據需要在堆疊上),而不是在 OCaml 堆積上分配。只有在可以證明相關參考不會逸出其定義範圍時,才會發生這種情況。
此轉換會發現已知等於特殊化引數的閉包變數。此類閉包變數會被替換為特殊化引數;然後可以透過「移除未使用的閉包變數」步驟來移除閉包變數 (請參閱下文)。
Flambda 優化器會對運算式進行分類,以判斷運算式是否
這是透過判斷執行運算式時可能發生的效果和共效果來完成的。效果說明運算式可能如何影響世界;共效果說明世界可能如何影響運算式。
效果分類如下
共效果只有一種分類
編譯器假設,在符合資料相依性的情況下,具有無效果且無共效果的運算式可以相對於其他運算式重新排序。
能夠靜態配置的模組編譯 (例如,對應於整個編譯單元的模組,而不是依賴於執行階段計算值的 first-class 模組) 最初遵循用於位元組碼的策略。一系列 let 綁定 (可能穿插著任意效果) 包圍著一個會變成模組區塊的記錄建立。Flambda 特有的轉換如下:這些綁定會提升為頂層符號,如上所述。
特別是在編寫在迴圈中執行非副作用演算法的效能評估套件時,可能會發現優化器完全省略了正在評估效能的程式碼。可以使用 Sys.opaque_identity 函式來防止此行為 (實際上它的行為與一般的 OCaml 函式相同,並且沒有任何「魔術」語義)。應查閱 Sys 模組的文件以取得更多詳細資訊。
Flambda 簡化步驟的行為表示,某些不安全的操作 (在沒有 Flambda 或使用先前版本的編譯器時可能是安全的) 不得使用。這特別是指 Obj 模組中的函式。
特別是,禁止變更任何不是可變的值 (例如使用 Obj.set_field 或 Obj.set_tag)。(從 C stub 返回的值一律視為可變。) 如果編譯器偵測到此類寫入,則會發出警告 59 — 但它無法在所有情況下都發出警告。以下是一個會觸發警告的程式碼範例
let f x = let a = 42, x in (Obj.magic a : int ref) := 1; fst a
這不安全的原因是簡化步驟認為 fst a 持有值 42;而且確實必須如此,除非透過不安全的操作破壞了型別健全性。
如果必須編寫會觸發警告 59 的程式碼,但已知該程式碼實際上是正確的 (對於某些正確的定義而言),則可以使用 Sys.opaque_identity 在執行不安全操作之前包裝該值。執行此操作時必須格外小心,以確保在正確的位置新增不透明度。必須強調的是,這種使用 Sys.opaque_identity 的方式僅適用於例外情況。不應在一般程式碼中使用它,或嘗試引導優化器。
例如,此程式碼會返回整數 1
let f x = let a = Sys.opaque_identity (42, x) in (Obj.magic a : int ref) := 1; fst a
但是,下列程式碼仍然會返回 42
let f x = let a = 42, x in Sys.opaque_identity (Obj.magic a : int ref) := 1; fst a
Flambda 執行的高級別內聯可能會暴露先前認為正確的程式碼中的錯誤。例如,請小心不要新增聲稱某些可變值始終是立即的型別註解,如果可能進行不安全的操作將其更新為已裝箱的值。
本手冊的此章節使用以下術語。