#Linux动态巨细裁剪以及包巨细变大排查思绪

打印 上一主题 下一主题

主题 1568|帖子 1568|积分 4704

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
1 动态库裁剪

  库分为动态库和静态库,动态库是在程序运行时才加载,静态库是在编译时就加载到程序中。动态库的巨细通常比静态库小,由于动态库只包罗了程序需要的函数和数据,而静态库则包罗了所有的函数和数据。静态库可以理解为引入源码编译,链接器在链接过程中会自动分析需要可不需要的代码举行删除裁剪。因此静态库不存在包巨细题目(除了特定平台生成静态库过大导致无法生成库文件的题目)。
  动态库裁剪的思绪很简单:

  • 通过工具或者编译选项删除不必要的数据和代码;
  • 只导出需要的函数和数据;
  • 关闭不必要的语言特性,如C++的异常处理等;
  • 优化代码,好比能用constexpr实现的只管用constexpr实现;
1.1 代码层面

  首先代码层面,需要尽可能确保不同模块之间的耦合度低,避免出现循环依赖的环境。其次,需要尽可能淘汰代码的重复,避免出现冗余代码的环境。末了,需要尽可能淘汰代码的复杂度,避免出现复杂的算法和数据布局的环境。对于一些可以大概用constexpr实现的功能,只管用constexpr实现,如许可以淘汰动态库的巨细。
  C++中轻易导致C++膨胀的代码:

  • 模板函数和模板类。模板函数和模板类在实例化时都会有一个对应版本的实例,如果任何函数都通过编译器的默认推导来实例化很轻易导致膨胀。因此模板函数和模板类应该只管避免使用默认推导,尽可能显示推导能淘汰实例化版本。因此可以使用范例擦除和显示实例化来解决模板膨胀的题目。
  • 内联函数。内联函数在编译时会被睁开,因此内联函数的代码会被复制到调用处,如许会导致代码膨胀。因此内联函数应该只管避免使用,除非函数的代码量很小。但是这一条对于现代C++ inline的含义已经发生了变化,inline优化基本完全由C++编译器自动优化。
  • 宏。宏在编译时会被替换,因此宏的代码会被复制到调用处,如许会导致代码膨胀。因此宏应该只管避免使用,除非宏的代码量很小。
  • 异常处理。异常处理会导致代码膨胀,由于异常处理需要在运行时举行,因此异常处理会导致代码膨胀。因此异常处理应该只管避免使用,除非异常处理的代码量很小。异常处理通常需要存储异常栈回溯相关的信息,因此轻易导致代码膨胀。
  • RTTI。RTTI 答应在运行时获取对象的范例信息。 RTTI 需要在代码中插入额外的范例信息,这会增长二进制文件的巨细。
  • 虚函数表。虚函数表是一个指针数组,它包罗了虚函数的地点。虚函数表需要在运行时举行查找,这会增长二进制文件的巨细。但是一般环境下,虚函数表的巨细是固定的,因此虚函数表的巨细并不是二进制膨胀的主要缘故原由。
1.2 编译选项

  通过编译选项可以控制编译器的行为,从而控制编译过程中的优化和裁剪。编译选项通常是通过编译器的命令行参数来设置的。常用的低落二进制巨细的编译选项有:

  • 优化品级,在编译动态库时,使用 -O2 或 -O3 优化级别。 这些优化级别可以使编译器生成更紧凑的代码,从而减小动态库的巨细。或者使用-Os之类均衡性能和巨细的选项。
  • 代码裁剪。

    • -function-sections:将每个函数放入单独的代码段。
    • -gc-sections:在链接时删除未使用的代码段。
    • -Wl,--gc-sections:在链接时删除未使用的代码段。

  • LTO。使用链接时优化(Link-Time Optimization, LTO)可以进一步减小动态库的巨细。 LTO 答应编译器在链接时举行全局优化,从而消除冗余代码和数据。

    • -flto:启用 LTO 优化。
    • -fwhole-program:启用 LTO 优化。

1.3 导出符号

  导出符号是指动态库中可以被其他模块(比方可实行文件或其他动态库)访问的函数和变量。 换句话说,它们是库的公共接口。默认环境下,在 Linux 系统中,使用 GCC 或 Clang 编译动态库时,所有非 static 的函数和全局变量都会被导出。 这通常会导致导出过多的符号,增长库的巨细。导出符号越多,库的巨细越大。 通过只导出必要的符号,可以显着减小库的巨细。
  控制导出符号不同编译器提供的方式不同,但是一般来说,有以下几种方式:

  • 通过导出文件指定导出的符号列表;
  • 代码中通过标记来标记需要导出的函数。
  1. #ifndef MY_LIBRARY_EXPORT_H
  2. #define MY_LIBRARY_EXPORT_H
  3. #ifdef _WIN32
  4.   #ifdef MY_LIBRARY_BUILD
  5.     #define MY_EXPORT __declspec(dllexport)
  6.   #else
  7.     #define MY_EXPORT __declspec(dllimport)
  8.   #endif
  9. #elif defined(__GNUC__)
  10.   #define MY_EXPORT __attribute__((visibility("default")))
  11. #else
  12.   #define MY_EXPORT
  13. #endif
  14. #endif // MY_LIBRARY_EXPORT_H
复制代码
1.4 strip

  通常环境下,二进制产物会包罗一些调试信息,好比符号表、调试符号等。这些信息对于调试和分析二进制文件非常有用,但是它们通常不会被用于发布版本。因此,在发布版本中,通常会使用strip工具来去除这些调试信息,从而减小二进制文件的巨细。


  • 不可逆操纵: strip命令会直接修改文件,并且无法恢复。 因此,在运行 strip命令之前,请务必备份文件。
  • 影响调试: 移除符号表和调试信息会使调试变得更加困难。 如果需要调试程序,请不要运行 strip命令。
  • 发布版本: strip命令通常用于发布终极版本的程序,以减小文件巨细并提高安全性。
  • 调试信息分离: 可以使用 --only-keep-debug和 --add-gnu-debuglink选项将调试信息分离到单独的文件中。 如许可以在不影响程序运行的环境下举行调试。
2 实行

2.1 测试代码和环境

  我们的测试环境是:
  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
  2. g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
复制代码
  测试代码如下,分别是一个头文件和一个源文件编译成so库:
  1. // my_lib.h
  2. #ifndef MY_LARGE_LIBRARY_H
  3. #define MY_LARGE_LIBRARY_H
  4. #include <iostream>
  5. #include <vector>
  6. // 用于控制导出符号,可以参考之前的通用 EXPORT 宏
  7. #ifdef _WIN32
  8.   #ifdef MY_LARGE_LIBRARY_BUILD
  9.     #define MY_LARGE_LIBRARY_API __declspec(dllexport)
  10.   #else
  11.     #define MY_LARGE_LIBRARY_API __declspec(dllimport)
  12.   #endif
  13. #elif defined(__GNUC__)
  14.   #define MY_LARGE_LIBRARY_API __attribute__((visibility("default")))
  15. #else
  16.   #define MY_LARGE_LIBRARY_API
  17. #endif
  18. // 模板类
  19. template <typename T>
  20. class MY_LARGE_LIBRARY_API MyTemplateClass {
  21. public:
  22.     MyTemplateClass(T value);
  23.     T getValue() const;
  24. private:
  25.     T m_value;
  26. };
  27. // 内联函数
  28. inline int MY_LARGE_LIBRARY_API inlineFunction(int x) {
  29.     return x * x * x; // 复杂的计算,增加内联的代价
  30. }
  31. // 虚基类
  32. class MY_LARGE_LIBRARY_API BaseClass {
  33. public:
  34.     BaseClass(int id);
  35.     virtual ~BaseClass();
  36.     virtual int calculate() const;
  37.     int getId() const;
  38. protected:
  39.     int m_id;
  40. };
  41. // 派生类
  42. class MY_LARGE_LIBRARY_API DerivedClass : public BaseClass {
  43. public:
  44.     DerivedClass(int id, double factor);
  45.     ~DerivedClass() override;
  46.     int calculate() const override;
  47. private:
  48.     double m_factor;
  49. };
  50. // 一个导出函数,使用了上述的类和函数
  51. MY_LARGE_LIBRARY_API int processData(const std::vector<int>& data);
  52. #endif // MY_LARGE_LIBRARY_H
复制代码
  1. // my_lib.cpp
  2. #include "Mylib.hpp"
  3. #include <numeric> // std::accumulate
  4. // 模板类的实现
  5. template <typename T>
  6. MyTemplateClass<T>::MyTemplateClass(T value) : m_value(value) {}
  7. template <typename T>
  8. T MyTemplateClass<T>::getValue() const {
  9.     return m_value;
  10. }
  11. // 显式实例化一些常用的模板类型,减少编译单元间的重复实例化
  12. template class MY_LARGE_LIBRARY_API MyTemplateClass<int>;
  13. template class MY_LARGE_LIBRARY_API MyTemplateClass<double>;
  14. // 基类的实现
  15. BaseClass::BaseClass(int id) : m_id(id) {}
  16. BaseClass::~BaseClass() {}
  17. int BaseClass::calculate() const {
  18.     return m_id * 2;
  19. }
  20. int BaseClass::getId() const {
  21.     return m_id;
  22. }
  23. // 派生类的实现
  24. DerivedClass::DerivedClass(int id, double factor) : BaseClass(id), m_factor(factor) {}
  25. DerivedClass::~DerivedClass() {}
  26. int DerivedClass::calculate() const {
  27.     return static_cast<int>(m_id * m_factor * 3);
  28. }
  29. // processData 函数的实现
  30. int processData(const std::vector<int>& data) {
  31.     int sum = std::accumulate(data.begin(), data.end(), 0);
  32.     int inlinedResult = inlineFunction(sum);
  33.     MyTemplateClass<int> templateObject(inlinedResult);
  34.     BaseClass* baseObject = new DerivedClass(sum, 2.5);
  35.     int finalResult = templateObject.getValue() + baseObject->calculate();
  36.     delete baseObject;
  37.     return finalResult;
  38. }
复制代码
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 包巨细排查思绪

  下面我们就简单排查下。
  根据上面的数据我们可以大概看到有两个选项导致了包巨细变大,分别是-O3和gc-sections,前者是由于该选项更倾向于优化性能而捐躯存储空间,因此已经有明确的结论不需要我们去排查。但是我们期望gc-sections等选项带来的是包巨细优化,但是事实却不是如此。
  首先,对于一个二进制动态库,其有不同的section组成,为了确认包巨细变大的缘故原由我们首先要做的是确认是哪个section变大了。因此我们使用objdump -h工具拆分二进制包来确认哪个部分增大了。下面是拆分得到的效果:
  1. 27 .debug_aranges     00000080 0000000000000000 DEBUG
  2. 30 .debug_line        000005f1 0000000000000000 DEBUG
  3. 31 .debug_str         00003bbe 0000000000000000 DEBUG
  4. 33 .debug_ranges      00000180 0000000000000000 DEBUG
  5. 27 .debug_aranges     00000110 0000000000000000 DEBUG
  6. 30 .debug_line        0000055f 0000000000000000 DEBUG
  7. 31 .debug_str         00003bae 0000000000000000 DEBUG
  8. 33 .debug_ranges      000000f0 0000000000000000 DEBUG
复制代码
  从上面的拆包可以大概看到增长的主要是调试信息。而这部分调试信息在后续的strip中已经被删除了,因此影响我们终极产物巨细的额外因素已经被排除了。如果希望知道详细增大了什么可以通过相关的提取对应section的信息来确认哪一部分增大了。
  上面的排查路径实在不是很典型,由于一般环境下包巨细都是由于代码引起的
  下面简单形貌下如何排查包巨细题目:

  • 首先,对比的产物一定是相同编译参数下的终极产物,使用两个带调试信息的不同编译参数的包对比没有意义(因此排查的条件是代码相同编译参数不同或者编译参数相同代码更改);
  • 准备好后,使用objdump -h分析不同section的巨细,来确认方向:

    • 不同section对应不同的数据,一般环境下比力轻易出现增大的是data和text段
    • .text: 代码段,包罗可实行指令。 如果包巨细增长主要是 .text section 变大,则需要关注代码优化。
    • .rodata: 只读数据段,包罗字符串常量、只读变量等。 大量的字符串常量或嵌入式资源会增长此 section 的巨细。
    • .data: 已初始化数据段,包罗已初始化的全局变量和静态变量。 大的静态数组或全局变量会增长此 section 的巨细。

  • 明确详细包巨细变化比力大的section后,可以尝试对比代码变更来开端确定变大的根本缘故原由,如果无法确定则继承;
  • 使用命令nm -CS <your_binary> | sort -rnk1 对代码段和数据段举行排序,然后对比不同版本之间的差异。
  • 找到差异的详细部分之后再使用objdump -d反汇编并对比源码来确认终极缘故原由。
   emsp; 需要注意的是,有些博客会保举使用bloaty,个人发起如果可以大概通过该工具排查发现数据异常,保举直接使用linux native的工具链。(在实际项目中发现bloaty好像统计的不是很准确。)

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

tsx81429

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表