[英]Static linking vs dynamic linking
在某些情況下,是否有任何令人信服的性能理由來選擇 static 鏈接而不是動態鏈接,反之亦然? 我聽過或讀過以下內容,但我對這個主題的了解還不夠,無法保證其真實性。
1) static 鏈接和動態鏈接之間的運行時性能差異通常可以忽略不計。
2) (1) 如果使用使用配置文件數據優化程序熱路徑的分析編譯器則不正確,因為通過 static 鏈接,編譯器可以同時優化您的代碼和庫代碼。 使用動態鏈接只能優化您的代碼。 如果大部分時間都花在運行庫代碼上,這會產生很大的不同。 否則,(1) 仍然適用。
一些編輯以在評論和其他答案中包含非常相關的建議。 我想指出,您打破這一點的方式在很大程度上取決於您計划運行的環境。最小的嵌入式系統可能沒有足夠的資源來支持動態鏈接。 稍微大一點的小型系統可能很好地支持動態鏈接,因為它們的內存小到足以使動態鏈接節省的 RAM 非常有吸引力。 正如馬克指出的那樣,成熟的消費類 PC 擁有巨大的資源,您可能可以讓便利性問題驅動您對此問題的思考。
解決性能和效率問題:這取決於.
傳統上,動態庫需要某種膠水層,這通常意味着函數尋址中的雙重調度或額外的間接層,並且可能會降低速度(但函數調用時間實際上是運行時間的重要組成部分嗎???)。
但是,如果您運行的多個進程都大量調用同一個庫,則在使用動態鏈接相對於使用靜態鏈接時,您最終可以節省緩存行(從而贏得運行性能)。 (除非現代操作系統足夠聰明,可以注意到靜態鏈接二進制文件中的相同段。看起來很難,有人知道嗎?)
另一個問題:加載時間。 您在某個時候支付裝載費用。 您何時支付此費用取決於操作系統的工作方式以及您使用的鏈接。 也許你寧願推遲支付,直到你知道你需要它。
請注意,靜態與動態鏈接傳統上不是優化問題,因為它們都涉及到目標文件的單獨編譯。 但是,這不是必需的:原則上,編譯器可以最初將“靜態庫”“編譯”為消化后的 AST 形式,然后通過將這些 AST 添加到為主代碼生成的 AST 來“鏈接”它們,從而實現全局優化。 我使用的系統都沒有這樣做,所以我無法評論它的工作情況。
回答性能問題的方法總是通過測試(並盡可能使用與部署環境相似的測試環境)。
1) 基於這樣一個事實,即調用 DLL 函數總是使用額外的間接跳轉。 今天,這通常可以忽略不計。 在 DLL 內部,i386 CPU 的開銷更大,因為它們無法生成位置無關代碼。 在 amd64 上,跳轉可以相對於程序計數器,所以這是一個巨大的改進。
2)這是正確的。 通過分析指導的優化,您通常可以獲得大約 10-15% 的性能。 現在 CPU 速度已達到極限,可能值得這樣做。
我要補充的是:(3) 鏈接器可以將函數安排在一個更高效的緩存分組中,從而最大限度地減少昂貴的緩存級別未命中。 它也可能特別影響應用程序的啟動時間(基於我使用 Sun C++ 編譯器看到的結果)
並且不要忘記,使用 DLL 無法消除死代碼。 根據語言的不同,DLL 代碼也可能不是最佳的。 虛函數始終是虛函數,因為編譯器不知道客戶端是否正在覆蓋它。
由於這些原因,如果真的不需要 DLL,那么只需使用靜態編譯。
編輯(回答評論,由用戶下划線)
這是一個關於位置無關代碼問題的好資源http://eli.thegreenplace.net/2011/11/03/position-independent-code-pic-in-shared-libraries/
正如所解釋的,x86 沒有 AFAIK 用於其他任何東西,然后是 15 位跳轉范圍,而不是用於無條件跳轉和調用。 這就是為什么具有超過 32K 的函數(來自生成器)一直是一個問題並且需要嵌入式蹦床的原因。
但是在像 Linux 這樣流行的 x86 操作系統上,您不需要關心 .so/DLL 文件是否不是使用gcc
開關-fpic
(強制使用間接跳轉表)生成的。 因為如果你不這樣做,代碼就像一個普通的鏈接器會重新定位它一樣被修復。 但是在這樣做時,它會使代碼段不可共享,並且需要將代碼從磁盤完整映射到內存並在使用之前將其全部接觸(清空大部分緩存,命中 TLB)等。 曾經有一段時間當這被認為是緩慢的。
這樣你就不會再有任何好處了。
我不記得是什么操作系統(Solaris 或 FreeBSD)給我的 Unix 構建系統帶來了問題,因為我只是沒有這樣做,並且想知道為什么在我將-fPIC
應用於gcc
之前它崩潰了。
動態鏈接是滿足某些許可要求(例如LGPL )的唯一實用方法。
我同意 dnmckee 提到的觀點,另外:
進行靜態鏈接構建的原因之一是驗證您對可執行文件已完全關閉,即所有符號引用都已正確解析。
作為使用持續集成構建和測試的大型系統的一部分,夜間回歸測試使用可執行文件的靜態鏈接版本運行。 有時,我們會看到符號無法解析並且靜態鏈接將失敗,即使動態鏈接的可執行文件鏈接成功。
這通常發生在位於共享庫中的符號的名稱拼寫錯誤時,因此不會靜態鏈接。 無論使用深度優先還是廣度優先評估,動態鏈接器都不會完全解析所有符號,因此您可以完成一個沒有完全閉包的動態鏈接可執行文件。
1/ 我一直在對動態鏈接與靜態鏈接進行基准測試的項目中,差異沒有確定到足以切換到動態鏈接(我不是測試的一部分,我只知道結論)
2/ 動態鏈接通常與PIC(Position Independent Code,不需要根據加載地址進行修改的代碼)相關聯。 根據架構,PIC 可能會帶來另一次減速,但為了獲得在兩個可執行文件之間共享動態鏈接庫的好處(如果操作系統使用加載地址隨機化作為安全措施,甚至同一可執行文件的兩個進程),則需要這樣做。 我不確定所有操作系統都允許將這兩個概念分開,但 Solaris 和 Linux 這樣做,而 HP-UX 也這樣做。
3/ 我一直在其他項目中使用動態鏈接來實現“簡單補丁”功能。 但是這個“簡單的補丁”使小補丁的分發變得更容易一些,而復雜的補丁則是版本控制的噩夢。 我們經常最終不得不推送所有內容,並且不得不在客戶站點跟蹤問題,因為錯誤的版本是令牌。
我的結論是,我使用了靜態鏈接除外:
對於依賴動態鏈接的插件之類的東西
當共享很重要時(多個進程同時使用的大型庫,如 C/C++ 運行時、GUI 庫……它們通常是獨立管理的,並且 ABI 是嚴格定義的)
如果有人想使用“簡單補丁”,我認為必須像上面的大庫一樣管理庫:它們必須幾乎獨立於定義的 ABI,並且不能被修復程序更改。
這將詳細討論 linux 上的共享庫和性能影響。
這很簡單,真的。 當您對源代碼進行更改時,您希望等待 10 分鍾來構建它還是等待 20 秒? 二十秒是我所能忍受的。 除此之外,我要么放棄劍,要么開始考慮如何使用單獨的編譯和鏈接將其帶回舒適區。
動態鏈接的最佳示例是,當庫依賴於使用的硬件時。 在古代,C 數學庫被決定為動態的,以便每個平台都可以使用所有處理器功能來優化它。
一個更好的例子可能是 OpenGL。 OpenGl 是一種由 AMD 和 NVidia 以不同方式實現的 API。 而且您不能在 AMD 卡上使用 NVidia 實現,因為硬件不同。 因此,您不能將 OpenGL 靜態鏈接到您的程序中。 這里使用動態鏈接讓 API 針對所有平台進行優化。
在類 Unix 系統上,動態鏈接會使“root”難以使用共享庫安裝在偏遠位置的應用程序。 這是因為動態鏈接器通常不會注意 LD_LIBRARY_PATH 或其對具有 root 權限的進程的等效項。 有時,靜態鏈接可以節省一天的時間。
或者,安裝過程必須定位庫,但這會使多個版本的軟件難以在機器上共存。
Static linking
是編譯時將鏈接內容復制到主二進制文件並成為單個二進制文件的過程。
缺點:
Dynamic linking
是加載鏈接內容時運行時的一個過程。 該技術允許:
ABI
穩定性[關於]缺點:
動態鏈接需要額外的時間讓操作系統找到動態庫並加載它。 使用靜態鏈接,一切都在一起,它是一次性加載到內存中。
另請參閱DLL 地獄。 在這種情況下,操作系統加載的 DLL 不是應用程序附帶的 DLL,也不是應用程序期望的版本。
另一個尚未討論的問題是修復庫中的錯誤。
使用靜態鏈接,您不僅需要重建庫,還必須重新鏈接和重新分發可執行文件。 如果該庫僅在一個可執行文件中使用,則這可能不是問題。 但是需要重新鏈接和重新分發的可執行文件越多,痛苦就越大。
使用動態鏈接,您只需重建和重新分發動態庫即可。
靜態鏈接將程序需要的文件包含在單個可執行文件中。
動態鏈接是您通常認為的,它使可執行文件仍然需要 DLL 並且位於同一目錄中(或者 DLL 可能位於系統文件夾中)。
(DLL =動態鏈接庫)
動態鏈接的可執行文件編譯速度更快,並且不會占用大量資源。
靜態鏈接只為您提供一個 exe,為了進行更改,您需要重新編譯整個程序。 而在動態鏈接中,您只需要對 dll 進行更改,當您運行 exe 時,將在運行時獲取更改。通過動態鏈接(例如:Windows)更容易提供更新和錯誤修復。
有大量且越來越多的系統,其中極端級別的靜態鏈接可以對應用程序和系統性能產生巨大的積極影響。
我指的是通常稱為“嵌入式系統”的東西,其中許多現在越來越多地使用通用操作系統,而這些系統用於所有可以想象的事情。
一個非常常見的例子是使用 GNU/Linux 系統的設備使用Busybox 。 我通過構建一個包含內核和根文件系統的可引導 i386(32 位)系統映像,在NetBSD 中將這一點發揮到極致,后者包含一個帶有硬鏈接的靜態鏈接(通過crunchgen
)二進制文件所有程序本身包含所有(最后是 274 個)標准的全功能系統程序(除了工具鏈之外的大多數),並且它的大小小於 20兆字節(並且可能在只有64MB 的內存(即使根文件系統未壓縮且完全在 RAM 中),但我一直無法找到這么小的內存來測試它)。
在之前的文章中已經提到靜態鏈接的二進制文件的啟動時間更快(並且可以快很多),但這只是圖片的一部分,尤其是當所有目標代碼都鏈接到同一個文件,尤其是當操作系統支持直接從可執行文件中分頁代碼時。 在這種理想的情況下程序的啟動時間是從字面上可以忽略不計,因為代碼幾乎所有頁面都已經在內存中,並在使用由外殼(與和init
可能正在運行任何其他后台進程),即使所請求的程序有自啟動以來從未運行過,因為可能只需要加載一頁內存來滿足程序的運行時要求。
然而,這還不是全部。 我還通常通過靜態鏈接所有二進制文件為我的完整開發系統構建和使用 NetBSD 操作系統安裝。 即使這需要大量更多的磁盤空間(x86_64 總共約 6.6GB,包括工具鏈和 X11 靜態鏈接)(特別是如果一個完整的調試符號表可用於所有程序,另外約 2.5GB),結果仍然整體運行速度更快,對於某些任務,甚至比典型的旨在共享庫代碼頁的動態鏈接系統使用更少的內存。 磁盤是便宜(甚至快速的磁盤),以及內存來緩存頻繁使用的磁盤文件也相對便宜,但CPU周期真的不是,並支付了ld.so
每個進程啟動會在每次啟動需要幾個小時的時間啟動成本和幾個小時的 CPU 周期遠離需要啟動多個進程的任務,特別是當重復使用相同的程序時,例如開發系統上的編譯器。 靜態鏈接的工具鏈程序可以將我的系統的整個操作系統多架構構建時間減少數小時。 我還沒有將工具鏈構建到我的單個crunchgen
ed 二進制文件中,但我懷疑當我這樣做時,由於 CPU 緩存的勝利,會節省更多的構建時間。
另一個考慮因素是您在庫中實際使用的目標文件(翻譯單元)的數量與可用的總數。 如果一個庫是由許多目標文件構建的,但你只使用其中幾個的符號,這可能是支持靜態鏈接的一個論據,因為你只鏈接你在靜態鏈接時使用的對象(通常)而不是“ t 通常攜帶未使用的符號。 如果您使用共享庫,則該庫包含所有翻譯單元,並且可能比您想要或需要的大得多。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.