#Linux動態大小裁剪以及包大小變大排查思路

1 動態庫裁剪

??庫分為動態庫和靜態庫,動態庫是在程序運行時才加載,靜態庫是在編譯時就加載到程序中。動態庫的大小通常比靜態庫小,因為動態庫只包含了程序需要的函數和數據,而靜態庫則包含了所有的函數和數據。靜態庫可以理解為引入源碼編譯,鏈接器在鏈接過程中會自動分析需要可不需要的代碼進行刪除裁剪。因此靜態庫不存在包大小問題(除了特定平臺生成靜態庫過大導致無法生成庫文件的問題)。

??動態庫裁剪的思路很簡單:

  1. 通過工具或者編譯選項刪除不必要的數據和代碼;
  2. 只導出需要的函數和數據;
  3. 關閉不必要的語言特性,如C++的異常處理等;
  4. 優化代碼,比如能用constexpr實現的盡量用constexpr實現;

1.1 代碼層面

??首先代碼層面,需要盡可能確保不同模塊之間的耦合度低,避免出現循環依賴的情況。其次,需要盡可能減少代碼的重復,避免出現冗余代碼的情況。最后,需要盡可能減少代碼的復雜度,避免出現復雜的算法和數據結構的情況。對于一些能夠用constexpr實現的功能,盡量用constexpr實現,這樣可以減少動態庫的大小。

??C++中容易導致C++膨脹的代碼:

  1. 模板函數和模板類。模板函數和模板類在實例化時都會有一個對應版本的實例,如果任何函數都通過編譯器的默認推導來實例化很容易導致膨脹。因此模板函數和模板類應該盡量避免使用默認推導,盡可能顯示推導能減少實例化版本。因此可以使用類型擦除和顯示實例化來解決模板膨脹的問題。
  2. 內聯函數。內聯函數在編譯時會被展開,因此內聯函數的代碼會被復制到調用處,這樣會導致代碼膨脹。因此內聯函數應該盡量避免使用,除非函數的代碼量很小。但是這一條對于現代C++ inline的含義已經發生了變化,inline優化基本完全由C++編譯器自動優化。
  3. 宏。宏在編譯時會被替換,因此宏的代碼會被復制到調用處,這樣會導致代碼膨脹。因此宏應該盡量避免使用,除非宏的代碼量很小。
  4. 異常處理。異常處理會導致代碼膨脹,因為異常處理需要在運行時進行,因此異常處理會導致代碼膨脹。因此異常處理應該盡量避免使用,除非異常處理的代碼量很小。異常處理通常需要存儲異常棧回溯相關的信息,因此容易導致代碼膨脹。
  5. RTTI。RTTI 允許在運行時獲取對象的類型信息。 RTTI 需要在代碼中插入額外的類型信息,這會增加二進制文件的大小。
  6. 虛函數表。虛函數表是一個指針數組,它包含了虛函數的地址。虛函數表需要在運行時進行查找,這會增加二進制文件的大小。但是一般情況下,虛函數表的大小是固定的,因此虛函數表的大小并不是二進制膨脹的主要原因。

1.2 編譯選項

??通過編譯選項可以控制編譯器的行為,從而控制編譯過程中的優化和裁剪。編譯選項通常是通過編譯器的命令行參數來設置的。常用的降低二進制大小的編譯選項有:

  1. 優化等級,在編譯動態庫時,使用 -O2 或 -O3 優化級別。 這些優化級別可以使編譯器生成更緊湊的代碼,從而減小動態庫的大小。或者使用-Os之類平衡性能和大小的選項。
  2. 代碼裁剪。
    1. -function-sections:將每個函數放入單獨的代碼段。
    2. -gc-sections:在鏈接時刪除未使用的代碼段。
    3. -Wl,--gc-sections:在鏈接時刪除未使用的代碼段。
  3. LTO。使用鏈接時優化(Link-Time Optimization, LTO)可以進一步減小動態庫的大小。 LTO 允許編譯器在鏈接時進行全局優化,從而消除冗余代碼和數據。
    1. -flto:啟用 LTO 優化。
    2. -fwhole-program:啟用 LTO 優化。

1.3 導出符號

??導出符號是指動態庫中可以被其他模塊(例如可執行文件或其他動態庫)訪問的函數和變量。 換句話說,它們是庫的公共接口。默認情況下,在 Linux 系統中,使用 GCC 或 Clang 編譯動態庫時,所有非 static 的函數和全局變量都會被導出。 這通常會導致導出過多的符號,增加庫的大小。導出符號越多,庫的大小越大。 通過只導出必要的符號,可以顯著減小庫的大小。

??控制導出符號不同編譯器提供的方式不同,但是一般來說,有以下幾種方式:

  1. 通過導出文件指定導出的符號列表;
  2. 代碼中通過標記來標記需要導出的函數。
#ifndef MY_LIBRARY_EXPORT_H
#define MY_LIBRARY_EXPORT_H#ifdef _WIN32#ifdef MY_LIBRARY_BUILD#define MY_EXPORT __declspec(dllexport)#else#define MY_EXPORT __declspec(dllimport)#endif
#elif defined(__GNUC__)#define MY_EXPORT __attribute__((visibility("default")))
#else#define MY_EXPORT
#endif#endif // MY_LIBRARY_EXPORT_H

1.4 strip

??通常情況下,二進制產物會包含一些調試信息,比如符號表、調試符號等。這些信息對于調試和分析二進制文件非常有用,但是它們通常不會被用于發布版本。因此,在發布版本中,通常會使用strip工具來去除這些調試信息,從而減小二進制文件的大小。

  • 不可逆操作: strip命令會直接修改文件,并且無法恢復。 因此,在運行 strip命令之前,請務必備份文件。
  • 影響調試: 移除符號表和調試信息會使調試變得更加困難。 如果需要調試程序,請不要運行 strip命令。
  • 發布版本: strip命令通常用于發布最終版本的程序,以減小文件大小并提高安全性。
  • 調試信息分離: 可以使用 --only-keep-debug--add-gnu-debuglink選項將調試信息分離到單獨的文件中。 這樣可以在不影響程序運行的情況下進行調試。

2 實驗

2.1 測試代碼和環境

??我們的測試環境是:

Linux DESKTOP-JLHBOB4 4.4.0-19041-Microsoft #4355-Microsoft Thu Apr 12 17:37:00 PST 2024 x86_64 x86_64 x86_64 GNU/Linux
g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

??測試代碼如下,分別是一個頭文件和一個源文件編譯成so庫:

// my_lib.h
#ifndef MY_LARGE_LIBRARY_H
#define MY_LARGE_LIBRARY_H#include <iostream>
#include <vector>// 用于控制導出符號,可以參考之前的通用 EXPORT 宏
#ifdef _WIN32#ifdef MY_LARGE_LIBRARY_BUILD#define MY_LARGE_LIBRARY_API __declspec(dllexport)#else#define MY_LARGE_LIBRARY_API __declspec(dllimport)#endif
#elif defined(__GNUC__)#define MY_LARGE_LIBRARY_API __attribute__((visibility("default")))
#else#define MY_LARGE_LIBRARY_API
#endif// 模板類
template <typename T>
class MY_LARGE_LIBRARY_API MyTemplateClass {
public:MyTemplateClass(T value);T getValue() const;
private:T m_value;
};// 內聯函數
inline int MY_LARGE_LIBRARY_API inlineFunction(int x) {return x * x * x; // 復雜的計算,增加內聯的代價
}// 虛基類
class MY_LARGE_LIBRARY_API BaseClass {
public:BaseClass(int id);virtual ~BaseClass();virtual int calculate() const;int getId() const;
protected:int m_id;
};// 派生類
class MY_LARGE_LIBRARY_API DerivedClass : public BaseClass {
public:DerivedClass(int id, double factor);~DerivedClass() override;int calculate() const override;
private:double m_factor;
};// 一個導出函數,使用了上述的類和函數
MY_LARGE_LIBRARY_API int processData(const std::vector<int>& data);#endif // MY_LARGE_LIBRARY_H
// my_lib.cpp
#include "Mylib.hpp"
#include <numeric> // std::accumulate// 模板類的實現
template <typename T>
MyTemplateClass<T>::MyTemplateClass(T value) : m_value(value) {}template <typename T>
T MyTemplateClass<T>::getValue() const {return m_value;
}// 顯式實例化一些常用的模板類型,減少編譯單元間的重復實例化
template class MY_LARGE_LIBRARY_API MyTemplateClass<int>;
template class MY_LARGE_LIBRARY_API MyTemplateClass<double>;// 基類的實現
BaseClass::BaseClass(int id) : m_id(id) {}BaseClass::~BaseClass() {}int BaseClass::calculate() const {return m_id * 2;
}int BaseClass::getId() const {return m_id;
}// 派生類的實現
DerivedClass::DerivedClass(int id, double factor) : BaseClass(id), m_factor(factor) {}DerivedClass::~DerivedClass() {}int DerivedClass::calculate() const {return static_cast<int>(m_id * m_factor * 3);
}// processData 函數的實現
int processData(const std::vector<int>& data) {int sum = std::accumulate(data.begin(), data.end(), 0);int inlinedResult = inlineFunction(sum);MyTemplateClass<int> templateObject(inlinedResult);BaseClass* baseObject = new DerivedClass(sum, 2.5);int finalResult = templateObject.getValue() + baseObject->calculate();delete baseObject;return finalResult;
}

2.1.2 不同操作對二進制大小的影響

默認-O1-O2-O3-Os符號sectionltowholertti異常debugstrip包大小(Byte)
57400
53752
53560
54784
53464
53480
53936
23120
10408
10016
10016
9640
6008

??下面是不同配置的詳細說明:

  • 默認配置:使用默認的編譯選項和編譯方式,不進行任何裁剪和優化。
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylib.so
  • 使用不同優化選項對比,具體-O0-O1-O2-O3
  • 隱藏符號:使用-fvisibility=hidden選項隱藏所有符號。
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylibos_hidden.so -fvisibility=hidden -Os
  • 獨立section裁剪:使用-ffunction-sections-fdata-sections選項將每個函數和數據放入單獨的代碼段和數據段。
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylibos_sections.so -ffunction-sections -fdata-sections -Os
  • lto
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylibos_sections_lto.so -ffunction-sections -fdata-sections -Os -Wl,--gc-sections -flto
  • 更激進的優化:-fwhole-program
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylibos_sections_lto_whole.so -ffunction-sections -fdata-sections -Os -Wl,--gc-sections -flto -fwhole-program
  • 禁用RTTI:-fno-rtti
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylibos_sections_lto_whole_nortti.so -ffunction-sections -fdata-sections -Os -Wl,--gc-sections -flto -fwhole-program -fno-rtti
  • 禁用異常-fno-exceptions
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylibos_sections_lto_whole_nortti_noex.so -ffunction-sections -fdata-sections -Os -Wl,--gc-sections -flto -fwhole-program -fno-rtti -fno-exceptions
  • 分離調試信息:-gsplit-dwarf
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylibos_sections_lto_whole_nortti_noex_debuginfo.so -ffunction-sections -fdata-sections -Os -Wl,--gc-sections -flto -fwhole-program -fno-rtti -fno-exceptions -gsplit-dwarf
  • 刪除無用的信息:strip
    • strip -g -x -s mylib.so

??從上面的結果來看我們上面大部分操作都可以減少二進制,而且效果明顯,我們的庫從最開始的57400Byte減少到了6008Byte。能夠看到成效是非常明顯的。但是本來預期能夠降低包大小的操作沒有降低包大小的同時,反而增加了包大小這是為什么。

??實際工程中往往限制導出符號比較能夠降低包大小,上面的實驗沒有降低包大小的原因是因為我們的測試代碼非常簡單函數太少,因此包大小的優化效果不是很明顯。以及一些其他參數沒有降低包大小的原因也是因為我們的測試代碼比較簡單。

2.1 包大小排查思路

??下面我們就簡單排查下。
??根據上面的數據我們能夠看到有兩個選項導致了包大小變大,分別是-O3gc-sections,前者是因為該選項更傾向于優化性能而犧牲存儲空間,因此已經有明確的結論不需要我們去排查。但是我們期望gc-sections等選項帶來的是包大小優化,但是事實卻不是如此。
??首先,對于一個二進制動態庫,其有不同的section組成,為了確認包大小變大的原因我們首先要做的是確認是哪個section變大了。因此我們使用objdump -h工具拆分二進制包來確認哪個部分增大了。下面是拆分得到的結果:

27 .debug_aranges     00000080 0000000000000000 DEBUG
30 .debug_line        000005f1 0000000000000000 DEBUG
31 .debug_str         00003bbe 0000000000000000 DEBUG
33 .debug_ranges      00000180 0000000000000000 DEBUG27 .debug_aranges     00000110 0000000000000000 DEBUG
30 .debug_line        0000055f 0000000000000000 DEBUG
31 .debug_str         00003bae 0000000000000000 DEBUG
33 .debug_ranges      000000f0 0000000000000000 DEBUG

??從上面的拆包能夠看到增加的主要是調試信息。而這部分調試信息在后續的strip中已經被刪除了,因此影響我們最終產物大小的額外因素已經被排除了。如果希望知道具體增大了什么可以通過相關的提取對應section的信息來確認哪一部分增大了。

??上面的排查路徑其實不是很典型,因為一般情況下包大小都是因為代碼引起的

??下面簡單描述下如何排查包大小問題:

  1. 首先,對比的產物一定是相同編譯參數下的最終產物,使用兩個帶調試信息的不同編譯參數的包對比沒有意義(因此排查的前提是代碼相同編譯參數不同或者編譯參數相同代碼更改);
  2. 準備好后,使用objdump -h分析不同section的大小,來確認方向:
    • 不同section對應不同的數據,一般情況下比較容易出現增大的是data和text段
    • .text: 代碼段,包含可執行指令。 如果包大小增加主要是 .text section 變大,則需要關注代碼優化。
    • .rodata: 只讀數據段,包含字符串常量、只讀變量等。 大量的字符串常量或嵌入式資源會增加此 section 的大小。
    • .data: 已初始化數據段,包含已初始化的全局變量和靜態變量。 大的靜態數組或全局變量會增加此 section 的大小。
  3. 明確具體包大小變化比較大的section后,可以嘗試對比代碼變動來初步確定變大的根本原因,如果無法確定則繼續;
  4. 使用命令nm -CS <your_binary> | sort -rnk1 對代碼段和數據段進行排序,然后對比不同版本之間的差異。
  5. 找到差異的具體部分之后再使用objdump -d反匯編并對比源碼來確認最終原因。

emsp;?需要注意的是,有些博客會推薦使用bloaty,個人建議如果能夠通過該工具排查發現數據異常,推薦直接使用linux native的工具鏈。(在實際項目中發現bloaty似乎統計的不是很準確。)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/77281.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/77281.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/77281.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

消息隊列生產者投遞的高可靠性與一致性保障方案

在構建高可靠分布式系統時&#xff0c;確保業務數據庫與消息隊列&#xff08;MQ&#xff09;之間的一致性是一項核心挑戰。尤其當使用 Kafka 作為消息隊列中間件時&#xff0c;如何避免“數據庫寫入成功&#xff0c;但消息發送失敗”或“消息重復發送”等問題&#xff0c;成為系…

Formality:Bug記錄

相關閱讀 Formalityhttps://blog.csdn.net/weixin_45791458/category_12841971.html?spm1001.2014.3001.5482 本文記錄博主在使用Synopsys的形式驗證工具Formality中遇到的一個Bug。 Bug復現 情況一 // 例1 module dff (input clk, input d_in, output d_out …

通信算法之267 : DJI無人機 云哨 DroneID 640ms

DJI 無人機 與DroneID 轉 *** 載 0x01 摘要 消費級無人機可以用于高級航拍、物流和人道主義救援等等。但是其廣泛使用給安全、安保和隱私帶來了許多風險。例如&#xff0c;攻擊方可能會使用無人機進行監視、運輸非法物品&#xff0c;或通過侵入機場上方的封閉空域造成經濟損…

論壇測試報告

作者前言 &#x1f382; ??????&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ?&#x1f382; 作者介紹&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…

npx 的作用以及延伸知識(.bin目錄,npm run xx 執行)

文章目錄 前言原理解析1. npx 的作用2. 為什么會有 node_modules/.bin/lerna3. npx 的查找順序4. 執行流程總結1&#xff1a; 1. .bin 機制什么是 node_modules/.bin&#xff1f;例子 2. npx 的底層實現npx 是如何工作的&#xff1f;為什么推薦用 npx&#xff1f;npx 的特殊能力…

【c語言】深入理解指針3——回調函數

一、回調函數 回調函數&#xff1a;通過函數指針調用的函數. 當把一個函數的地址傳遞給另一個函數&#xff0c;通過該地址去調用其指向的函數&#xff0c;那么這個被調用的函數就是回調函數. 示例&#xff1a; 在【深入理解指針2】中結尾寫了用函數指針實現計算器的功能&#…

HTTP 核心概念

&#x1f9d1; 博主簡介&#xff1a;CSDN博客專家&#xff0c;歷代文學網&#xff08;PC端可以訪問&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移動端可微信小程序搜索“歷代文學”&#xff09;總架構師&#xff0c;15年工作經驗&#xff0c;…

VidBot:從野外 2D 人體視頻中學習可泛化的 3D 動作,實現零樣本機器人操控

25年3月來自慕尼黑工大、瑞士 ETH 和微軟的論文“VidBot: Learning Generalizable 3D Actions from In-the-Wild 2D Human Videos for Zero-Shot Robotic Manipulation”。 未來的機器人被設想為能夠執行各種家務的多功能系統。最大的問題仍然是&#xff0c;如何在盡量減少機器…

Linux 日常運維命令大全

Linux 作為一種開源操作系統&#xff0c;在服務器運維中扮演著重要角色。掌握常用的 Linux 命令對于運維人員而言至關重要。本文將整理一份 Linux 服務器運維常用命令大全&#xff0c;幫助你在日常工作中提高效率和準確性。 1. 基礎命令 基礎命令是Linux操作的起點&#xff0…

編程規范之枚舉

編程規范之枚舉 1.1 初始化枚舉項 枚舉平時用的也沒有很頻繁&#xff0c;今天看代碼規范提到枚舉類型初始化枚舉項。并對初始化枚舉項進行了歸納。包括下面三個 不進行顯示初始化&#xff0c;交由編譯器完成。 對第一個枚舉項的顯式初始化&#xff0c;這樣可以強制整數值的…

《軟件設計師》復習筆記(12.1)——范圍管理、進度管理

目錄 一、范圍管理 1. 核心概念 2. 范圍管理過程 WBS&#xff08;工作分解結構&#xff09;示例 真題示例&#xff1a; 二、進度管理 1. 核心過程 2. 關鍵工具與技術 真題示例&#xff1a; 一、范圍管理 1. 核心概念 項目范圍&#xff1a;為交付產品必須完成的工作…

過去十年前端框架演變與技術驅動因素剖析

一、技術演進脈絡&#xff08;2013-2023&#xff09; 2013-2015&#xff1a;結構化需求催生框架雛形 早期的jQuery雖然解決了跨瀏覽器兼容性問題&#xff08;如IE8兼容性處理&#xff09;&#xff0c;但其松散的代碼組織方式難以支撐復雜應用開發。Backbone.js的出現首次引入M…

中華傳承-醫山命相卜-梅花易數

梅花易數 靈活起卦&#xff08;如數字、聲音、外應等&#xff09;和象數結合&#xff0c;準確率可達96.8%。其起卦方式擺脫傳統龜殼、蓍草的繁瑣&#xff0c;強調直覺與靈活性。 個人決策、事件預測等 尤其在短期、具體問題上表現突出。

如何用Brower Use WebUI實現網頁數據智能抓取與分析?

作者&#xff1a;算力魔方創始人/英特爾創新大使劉力 Browser-use是一款能讓AI智能體像人類一樣操作網頁的創新工具&#xff0c;與傳統網絡爬蟲技術相比&#xff0c;Browser-use能模擬人瀏覽并操作網頁&#xff0c;在采集網站數據時&#xff0c;不會被網站反爬機制識別和封禁&…

LIMS引領綜合質檢中心數字化變革,賦能質量強國戰略

在質量強國戰略的深入推進下&#xff0c;我國綜合質檢機構迎來了前所未有的發展機遇&#xff0c;同時也面臨著諸多嚴峻挑戰。隨著檢測領域從傳統的食品藥品監督向環境監測、新材料檢測等新興領域不斷拓展&#xff0c;跨領域協同管理的復雜度呈指數級增長。作為提升產品質量的關…

簡單好用的在線工具

用AI寫了一些在線工具&#xff0c;簡介好用&#xff0c;推薦給大家&#xff0c;歡迎大家使用并提議意見。 網址&#xff1a;https://www.bittygarden.com/ 目前已有以下功能&#xff1a; MD5SM3SHAUnicode 編碼Unicode 解碼Base32 編碼Base32 解碼Base64 編碼Base64 解碼URL …

阿里云服務器搭建開源版禪道

一&#xff0c;下載地址&#xff1a;禪道11.5版本發布&#xff0c;主要完善細節&#xff0c;修復bug&#xff0c;新增動態過濾機制 - 禪道下載 - 禪道項目管理軟件 下載地址二&#xff1a; 禪道21.6.stable 實現舊編輯器撰寫的文檔無感升級至新版編輯器 - 禪道下載 - 禪道項目…

leetcode 309. Best Time to Buy and Sell Stock with Cooldown

目錄 題目描述 第一步&#xff0c;明確并理解dp數組及下標的含義 第二步&#xff0c;分析并理解遞推公式 1.求dp[i][0] 2.求dp[i][1] 3.求dp[i][2] 第三步&#xff0c;理解dp數組如何初始化 第四步&#xff0c;理解遍歷順序 代碼 題目描述 這道題與第122題的區別就是賣…

嵌入式硬件常用總線接口知識體系總結和對比

0.前言 在嵌入式工程實現中,多多少少我們都使用過總線,各種各樣的總線應用于不同場合,不同場景有不同的優勢,但是我們在作為工程師過程中在如何選擇項目合適的總線,根據什么來選?需要我們對項目全局和總線特征有所了解,本文目的就是對比多種總線的關鍵特征 我們在聊到…

數據分析處理庫Pandas常用方法匯總

目錄 一、基礎操作 1.1 創建df對象 1.1.1 讀入表格數據 1.1.2 手動創建df 1.2 .info() 1.3 df.index 1.4 df.columns 1.5 df.dtypes 1.6 df.values 1.7 .set_index() 1.8 df[xxx] 1.9 .describe() 1.10 .isin() 1.12 .where() 1.13 .query() 1.14 Series類型運算…