編譯 Julia 套件來避免過長的載入時間
Julia 有優異的效能,但是它的 JIT 讓套件的載入時間(using
)過於冗長。
Julia JIT 會在套件載入時期以及第一次使用函式時期進行編譯,若是有編譯完成的版本,便可以直接使用,避免編譯時間。儘管 Julia 將程式碼進行編譯,得到了絕佳的執行時間,但是套件載入的第一次編譯時間成為了大家的痛點。
為了解決這樣的痛點,開發者社群做了不少努力,主要是 PackageCompiler.jl 套件,讓套件可以預先進行編譯,而後來也補充了不少 binary package 來支援套件編譯。
PackageCompiler.jl 套件可以將 Julia 程式碼編譯成可執行檔以及動態函式庫(.so),支援套件編譯成動態函式庫,以及增量編譯(incremental compilation)。
近期 PackageCompiler.jl 套件正式發佈 v1.0,正式成熟可以使用了!本文將會介紹如何將套件編譯成動態函式庫,並且在啟動時載入,來避免過長的載入及編譯時間。
動態語言編譯
在動態語言中,編譯一直是一個大問題。編譯時,需要知道更多底層的細節,像是變數的型別。例如 foo(a::Any, b::Any)
這樣的函式在動態語言中非常常見。Julia 會在呼叫 foo(3, 4)
時,經由型別推斷(type inference)知道它需要編譯 foo(a::Int64, b::Int64)
這樣的方法出來。當下次呼叫 foo(3., 4.)
時,則需要編譯 foo(a::Float64, b::Float64)
。然而,要完整的編譯 foo
這個函式,需要編譯多少種方法呢?在型別的排列組合上會造成組合爆炸的問題,讓需要編譯的方法多到難以處理。這一直以來是動態語言無法完整編譯的問題點。
那麼 Julia 怎麼做?PackageCompiler.jl 套件的編譯來自於 JIT,所以它直接將 Julia session 中已經編譯的函式儲存下來,以利後續使用。編譯的函式會以動態函式庫(.so)的形式儲存,就如同其他語言一樣。然而,Julia 仍然沒有完整編譯,Julia 只有部份編譯便存入動態函式庫。若是需要使用到未編譯的函式,就會在使用時 JIT 去處理。或許可以藉由套件的測試來增加編譯的函式數目,例如提高測試的覆蓋度來儘可能觸發各式各樣的方法進行編譯,然後儲存下來。
編譯套件
這邊我以編譯 Plots.jl 套件為例,繪圖相關套件的載入時間總是異常地久,所以我選擇 Plots.jl 套件來舉例。
請確定 Julia 中已經安裝 Plots.jl 套件。
1 | julia> using PackageCompiler |
create_sysimage
會編譯並產生 sysimage,這個 image 可以在 Julia 啟動時一併載入,它會以動態函式庫的形式儲存下來。第一個參數請指定需要編譯的套件名稱,可以同時編譯多個套件,例如 [:Plots, :Gadfly]
。關鍵字參數 sysimage_path="Plots.so"
需要指定 system image 的檔名。執行後需要一段時間進行編譯。
1 | shell> ls |
編譯完成後便可以在目前的資料夾下看到編譯完成的檔案 Plots.so。
在下次使用時使用的話,請在啟動 Julia 時加入參數 -JPlots.so
。
1 | julia> exit() |
如此,我們就可以看到在啟動 Julia 之後載入的套件就有 Plots.jl 囉!Plots.jl 套件的載入時間被包含在 Julia 啟動時間中。
1 | julia> Base.loaded_modules |
編譯到預設位置
Julia 在系統中有 sys.so,它預設會在 Julia 啟動時載入,我們可以利用預先編譯好的函式庫來取代它,就可以不用參數的情況下自動載入。
1 | create_sysimage([:Debugger, :OhMyREPL]; replace_default=true) |
將 sysimage_path
換成 replace_default=true
,來將編譯的函式庫取代 sys.so,它會將原本的 sys.so 備份到 sys.so.backup,並將編譯好的 sys.so 寫入。
增量編譯(incremental compilation)
sysimage 可以進行增量編譯。增量編譯是指既有的函式並不會重新編譯,只會編譯那些新加入的函式。如果已經有 sysimage,那麼下次呼叫 create_sysimage
時,只會編譯新加入的部份,並且用新的函式庫檔案取代舊的。
如果你想要關閉這個功能,那需要加入 incremental=false
來關閉增量編譯,這樣就會編譯一個全新的函式庫。
利用套件的測試程式碼做編譯
一般而言,在動態語言中,我們無法編譯所有可能的方法,取而代之的是,我們可以儘可能編譯更多方法。編譯更多方法越可能可以避免 Julia 啟動 JIT 在執行時期去編譯新的方法。這時候我們可以利用套件中的測試程式碼,當套件中自帶的測試程式碼,如果覆蓋率夠高,那麼就可以有越完善的編譯。這時候我們可以在編譯之前執行測試來促使 JIT 編譯各種方法。
我們可以利用以下程式碼來執行測試,並促使 JIT 進行編譯。
1 | import Plots |
套件的測試期間常常會有其他的相依套件,缺乏相依套件都會使測試失敗。需要不斷執行測試,並且檢查及安裝缺失的相依套件。
等測試都執行完成了,就可以進行套件編譯了。
編譯被使用的方法
如果需要編譯被使用的方法,例如在特定腳本被使用到,這時候我們需要更細緻的編譯方式。我們需要知道哪些方法在腳本當中被使用到,這時候我們可以在啟動 Julia 時加入 --trace-compile=precompile.jl
。--trace-compile
會讓 Julia 紀錄下哪些方法有被編譯過,並且輸出在 precompile.jl 檔案中紀錄下來。
--trace-compile
會紀錄哪些方法需要預編譯(precompilation)。例如執行 julia --trace-compile=precompile.jl -e '1+1'
會得到一個 precompile.jl 檔案,其中測試了 1+1
的預編譯,並且紀錄下來。因此,會在 precompile.jl 檔案中看到以下內容:
1 | precompile(Tuple{typeof(Base.similar), Array{Base.Grisu.Bignums.Bignum, 1}}) |
這就是 --trace-compile
將需要預編譯的方法輸出的結果。
我們可以將這些結果放到 create_sysimage
中。可以在 create_sysimage
加入關鍵字參數 precompile_statements_file="precompile.jl"
,並且指定剛剛輸出的預編譯紀錄檔 precompile.jl。它會讓 Julia 根據紀錄檔中所需要使用到的方法進行編譯。如此就可以編譯在腳本當中被使用到的方法了!
Ref: https://julialang.github.io/PackageCompiler.jl/dev/sysimages/