首頁
» 最佳化程式碼
一般而言,您應該先在不進行最佳化的情況下編譯和執行程式碼,這是您在未指定最佳化層級的情況下執行 emcc
時的預設值。這種未最佳化的建置包含一些檢查和斷言,這些檢查和斷言對於確保您的程式碼正確執行非常有幫助。一旦程式碼正確執行,強烈建議您最佳化要發布的建置版本,原因有幾個:首先,最佳化的建置版本會更小、更快,因此它們的載入速度更快且執行更順暢,其次,未最佳化的建置版本包含除錯資訊,例如檔案和函式的名稱、JavaScript 中的程式碼註解等 (除了增加大小之外,還可能包含您不希望發布給使用者的內容)。
本頁的其餘部分說明如何最佳化您的程式碼。
程式碼是透過在執行 emcc 時指定最佳化旗標來最佳化。這些層級包括:-O0 (不最佳化)、-O1、-O2、-Os、-Oz、-Og 和 -O3。
例如,若要使用最佳化層級 -O2
進行編譯
emcc -O2 file.cpp
較高的最佳化層級會逐步引入更積極的最佳化,從而提高效能和程式碼大小,但代價是編譯時間增加。這些層級還可以突顯與程式碼中未定義行為相關的不同問題。
您應該使用的最佳化層級主要取決於目前開發階段
當第一次移植程式碼時,請使用預設設定 (不進行最佳化) 在您的程式碼上執行 emcc。檢查您的程式碼是否正常運作,並在繼續之前除錯並修正任何問題。
在開發期間使用較低的最佳化層級進行建置,以縮短編譯/測試迭代週期 (-O0
或 -O1
)。
使用 -O2
進行建置以獲得經過良好最佳化的建置版本。
使用 -O3
或 -Os
進行建置可以產生比 -O2
更好的建置版本,而且值得在發行版本中考慮。-O3
建置版本比 -O2
更加最佳化,但代價是編譯時間顯著增加,而且程式碼大小可能會更大。-Os
在增加編譯時間方面類似,但著重於縮減程式碼大小,同時進行額外的最佳化。值得嘗試這些不同的最佳化選項,以了解哪種最適合您的應用程式。
下列章節會討論其他最佳化。
注意
emcc 最佳化旗標 (-O1、 -O2
等) 的含義與 gcc、clang 和其他編譯器相似,但也不同,因為最佳化 WebAssembly 包含一些額外的最佳化類型。參考文件中記錄了 emcc 層級到 LLVM 位元碼最佳化層級的對應。
將原始碼檔案編譯為物件檔案就像您在原生建置系統中使用 clang 和 LLVM 時所預期的一樣。當將物件檔案連結到最終的可執行檔時,Emscripten 也會根據最佳化層級執行額外的最佳化
執行 Binaryen 最佳化器。Binaryen 會對 Wasm 進行 LLVM 沒有的一般用途最佳化,也會執行一些整體程式最佳化。(請注意,Binaryen 的整體程式最佳化可能會執行內嵌之類的操作,這在某些情況下可能會令人驚訝,因為 LLVM IR 屬性 (例如 noinline
) 在此時已遺失。)
在此階段產生 JavaScript,並由 Emscripten 的 JS 最佳化器進行最佳化。您可以選擇性地執行 closure 編譯器,強烈建議使用此編譯器來縮減程式碼大小。
Emscripten 也會最佳化合併的 Wasm+JS,方法是縮減它們之間的匯入和匯出,並執行 meta-dce,這會移除跨越兩個世界週期中未使用的程式碼。
若要在連結時間略過額外的最佳化工作,請使用 -O0
或 -O1
進行連結。在這些模式下,Emscripten 著重於更快的迭代時間。(請注意,即使原始碼檔案是以不同的最佳化層級編譯,也可以使用這些旗標進行連結。)
若要在連結時間也略過非最佳化工作,請使用 -sWASM_BIGINT
進行連結。啟用 BigInt 支援會移除 Emscripten 在 JS/Wasm 邊界上將 Wasm「合法化」以處理 i64
值 (如同 BigInts,i64
值是合法的,而且不需要額外的處理) 的需求。
有些連結旗標會在連結階段新增額外的工作,這可能會降低速度。例如,-g
會啟用 DWARF 支援,-sSAFE_HEAP
之類的旗標會需要 JS 後處理,而 -sASYNCIFY
之類的旗標會需要 Wasm 後處理。若要確保您的旗標允許盡可能最快的連結,其中 Wasm 在 wasm-ld
之後不會被修改,請使用 -sERROR_ON_WASM_CHANGES_AFTER_LINK
進行建置。使用該選項,如果 Emscripten 必須對 Wasm 執行變更,您會在連結期間收到錯誤。例如,如果您沒有傳遞 -sWASM_BIGINT
,它會告訴您合法化會強制它變更 Wasm。如果您使用 -O2
或更高版本進行建置,您也會收到錯誤,因為通常會執行 Binaryen 最佳化器。
您可以傳遞給編譯器數個旗標來影響程式碼產生,這也會影響效能,例如 DISABLE_EXCEPTION_CATCHING。這些旗標記錄在 src/settings.js 中。
Emscripten 預設會發出 WebAssembly。您可以使用 -sWASM=0
關閉該功能 (在這種情況下,emscripten 會發出 JavaScript),如果您希望輸出在尚無 Wasm 支援的地方執行,則必須這樣做,但缺點是程式碼較大且速度較慢。
本節說明與程式碼大小相關的最佳化和問題。它們對於您希望達到最小占用空間的小型專案或程式庫,以及您想要避免龐大尺寸可能導致問題 (例如啟動速度緩慢) 的大型專案都非常有用。
您可能會希望在專案中使用 -Os 或 -Oz 來建置效能要求較不敏感的原始碼檔案,而其餘部分則使用 -O2。(-Os 和 -Oz 與 -O2 類似,但會犧牲效能來縮減程式碼大小。-Oz 比 -Os 更能縮減程式碼大小。)
另外,您可以使用 -Os
或 -Oz
執行最終的連結/建置命令,以讓編譯器在產生 WebAssembly 模組時更專注於程式碼大小。
除了以上方法,以下提示也有助於縮減程式碼大小
在未編譯的程式碼上使用 closure 編譯器:--closure 1
。這可以大幅縮減支援 JavaScript 程式碼的大小,強烈建議使用。然而,如果您加入自己的額外 JavaScript 程式碼(例如在 --pre-js
中),則需要確保程式碼正確使用 closure 註解。
Floh 關於此主題的部落格文章非常有幫助。
請務必在您的網頁伺服器上使用 gzip 壓縮,現在所有瀏覽器都支援這種壓縮。
以下編譯器設定可能會有所幫助(詳情請參閱 src/settings.js
)
盡可能停用內聯,使用 -sINLINING_LIMIT
。使用 -Os 或 -Oz 進行編譯通常也會避免內聯。(不過,內聯可以讓程式碼更快,因此請謹慎使用。)
您可以使用 -sFILESYSTEM=0
選項來停用檔案系統支援程式碼的綑綁(如果未使用,編譯器應該會將其最佳化掉,但可能並非總是成功)。如果您正在建置純粹的計算函式庫,這可能會很有用。
ENVIRONMENT
旗標可讓您指定輸出僅在網頁上執行,或僅在 node.js 中執行等等。這可以防止編譯器發出支援所有可能執行環境的程式碼,節省約 2KB。
連結時間最佳化 (LTO) 可讓編譯器執行更多最佳化,因為它可以在不同的編譯單元之間進行內聯,甚至可以使用系統函式庫。LTO 可透過使用 -flto
編譯物件檔案來啟用。此旗標的效果是發出 LTO 物件檔案(技術上這表示發出位元碼)。連結器可以處理混合的 Wasm 物件檔案和 LTO 物件檔案。在連結時傳遞 -flto
也會觸發使用 LTO 系統函式庫。
因此,為了讓 LLVM Wasm 後端擁有最大的 LTO 機會,請使用 -flto
建置所有原始碼檔案,並使用 flto
進行連結。
使用 -sEVAL_CTORS
進行建置將在編譯時評估盡可能多的程式碼。這包括「全域建構函式」函式(LLVM 發出在 main()
之前執行的函式)以及 main()
本身。盡可能多的程式碼將會被評估,然後產生的狀態會「快照」到 wasm 中。接著當程式執行時,它會從該狀態開始,而不需要執行該程式碼,這可以節省時間。
這種最佳化可能會縮減或增加程式碼大小。舉例來說,如果少量的程式碼在記憶體中產生許多變更,則整體大小可能會增加。最好使用此旗標進行建置,然後測量程式碼和啟動速度,看看在您的程式中這種取捨是否值得。
您可以盡力編寫對 EVAL_CTORS 友善的程式碼,盡可能延遲無法評估的內容。例如,對匯入的呼叫會停止這種最佳化,因此如果您有一個建立 GL 環境,然後執行一些純粹計算以在記憶體中設定不相關的資料結構的遊戲引擎,則可以反轉該順序。然後純粹的計算可以先執行,並被評估掉,而對匯入的 GL 環境建立呼叫不會阻止這種情況。您可以做的其他事情包括避免使用 argc/argv
、避免使用 getenv()
等等。
使用此選項時會顯示記錄,以便您可以查看是否可以改進。以下是 emcc -sEVAL_CTORS
的輸出範例
trying to eval __wasm_call_ctors
...partial evalling successful, but stopping since could not eval: call import: wasi_snapshot_preview1.environ_sizes_get
recommendation: consider --ignore-external-input
...stopping
第一行指出嘗試評估 LLVM 的函式,該函式會執行全域建構函式。它評估了函式的一部分,但隨後在 WASI 匯入 environ_sizes_get
上停止,這表示它嘗試從環境讀取。如輸出所示,您可以告知 EVAL_CTORS
忽略外部輸入,這將會忽略這些內容。您可以使用模式 2
來啟用該功能,也就是說,使用 emcc -sEVAL_CTORS=2
進行建置
trying to eval __wasm_call_ctors
...success on __wasm_call_ctors.
trying to eval main
...stopping (in block) since could not eval: call import: wasi_snapshot_preview1.fd_write
...stopping
現在它已成功完整評估 __wasm_call_ctors
。接著它移至 main
,由於呼叫 WASI 的 fd_write
(也就是說,呼叫列印內容),因此它在此停止。
上一節關於縮減程式碼大小的內容對於非常大的程式碼庫可能會有所幫助。此外,以下是一些其他可能有用的主題。
如果您在瀏覽器中遇到記憶體限制,則將專案獨立執行可能會有所幫助,而不是在包含其他內容的網頁中執行。如果您開啟一個新的網頁(以新的索引標籤或新的視窗),其中只包含您的專案,那麼您就有最大的機會避免記憶體片段化問題。
如果您的模組太大,導致下載和實例化的時間明顯影響應用程式的啟動效能,則可能值得分割模組,並延遲載入啟動應用程式不需要的程式碼。請參閱 模組分割,以取得如何執行此操作的指南。請注意,模組分割是一項實驗性功能,可能會有所變更。
在 -O1
(及以上)中,預設會關閉捕捉 C++ 例外(具體而言,是發出 catch 區塊)。由於 WebAssembly 目前實作例外的方式,這會使程式碼更小更快(最終,Wasm 應該會獲得對例外的原生支援,而不會有這個問題)。
若要在最佳化程式碼中重新啟用例外,請使用 -sDISABLE_EXCEPTION_CATCHING=0
執行 emcc(請參閱 src/settings.js)。
注意
當停用例外捕捉時,擲回的例外會終止應用程式。換句話說,仍然會擲回例外,但不會被捕捉。
注意
即使不發出 catch 區塊,除非您使用 -fno-exceptions
建置原始碼檔案,否則仍然會有一些程式碼大小的額外負荷,這會省略所有例外支援程式碼(例如,它會避免在 std::vector 的錯誤中建立正確的 C++ 例外物件,如果發生錯誤只會中止應用程式)
C++ 執行階段類型資訊支援(動態轉換等等)會增加有時不需要的額外負荷。例如,在 Box2D 中,不需要 rtti 或例外,如果您使用 -fno-rtti -fno-exceptions
建置原始碼檔案,則會縮減 15% 的輸出 (!)。
使用 -sALLOW_MEMORY_GROWTH
進行建置允許使用的總記憶體量根據應用程式的需求而變更。這對於預先不知道需要多少記憶體的應用程式很有用。
啟用 偵錯模式 (EMCC_DEBUG) 以輸出每個編譯階段的檔案,包括主要最佳化操作。
預設使用的 malloc/free
實作是 dlmalloc
。您也可以選擇 emmalloc
(-sMALLOC=emmalloc
),它體積較小但速度較慢,或是 mimalloc
(-sMALLOC=mimalloc
),它體積較大,但在多執行緒應用程式中,當 malloc/free
發生競爭時,效能擴展性較佳(請參閱配置器效能)。
以下是一些您可能想要嘗試的不安全最佳化:
--closure 1
:這有助於減少非產生(支援/黏合)的 JavaScript 程式碼大小,並加速啟動。然而,如果您沒有進行適當的 Closure Compiler 註解和匯出,它可能會失效。但這很值得嘗試!
現代瀏覽器具有 JavaScript 效能分析器,可以協助您找出程式碼中較慢的部分。由於每個瀏覽器的效能分析器都有其限制,因此強烈建議在多個瀏覽器中進行效能分析。
為了確保編譯後的程式碼包含足夠的效能分析資訊,請使用 效能分析 以及最佳化和其他標誌來建置您的專案
emcc -O2 --profiling file.cpp
Emscripten 編譯的程式碼通常可以接近原生建置的速度。如果效能明顯低於預期,您也可以執行以下其他疑難排解步驟