编译器工具链(五)——代码体积优化

代码体积优化

一直以来,嵌入式应用程序的代码体积一直是被关注的问题。如今,编译器在优化应用程序的代码体积方面已经取得了不小的进展。虽然,大多数编译器优化都主要集中在应用程序性能上,但近年来也有不少针对代码体积的优化。本文会从以下几个方面简单介绍一下减少应用程序代码体积的一些常用技术:

  • 测量方法:测量二进制大小的工具
  • 编译器优化:帮助减少应用程序二进制大小的编译选项
  • 源代码优化:减少应用程序二进制大小的编程技巧

测量方法

简单介绍一下3种常用的测量二进制文件大小的工具:

  1. size:可以显示二进制文件每个部分的大小。
$ size gcc/11/libstdc++.dylib

__TEXT    __DATA    __OBJC    others    dec    hex
1703936    65536    0    1851392    3620864    374000
  1. strings:可以显示二进制中的所有字符串。
$ strings gcc/11/libstdc++.dylib | wc -l

2180
  1. bloaty:可以用于对不同平台的二进制文件进行更深入的分析。甚至将代码体积标注到源文件中,从而帮助更好地发现缩减代码体积的机会。
$ ​bloaty gcc/11/libstdc++.dylib

     FILE SIZE     VM SIZE
--------------  --------------
 29.1%  1.00Mi  29.0%. 1.00Mi   __TEXT,__text
 25.0%   882Ki  25.0%   882Ki   String Table
 16.6%   583Ki  16.5%   583Ki   Symbol Table
 12.3%   433Ki  12.2%   433Ki   __TEXT,__eh_frame
  5.0%   176Ki   5.0%   176Ki   Export Info
  4.1%   146Ki   4.1%   146Ki   __TEXT,__const
  2.5%  87.8Ki   2.5%  87.8Ki   Weak Binding Info
  1.2%  41.6Ki   1.2%  41.6Ki   __DATA,__gcc_except_tab
  1.0%  36.9Ki   1.0%  36.9Ki   __DATA_CONST,__const
  0.9%  33.3Ki   0.9%  33.3Ki   __TEXT,__text_cold
  0.5%  16.1Ki   0.5%  16.1Ki   [10 Others]
  0.5%  15.9Ki   0.0%     945   [__DATA]
  0.4%  15.0Ki   0.4%  15.0Ki   __TEXT,__cstring
  0.0%    4      0.3%  11.3Ki   [__LINKEDIT]
  0.0%    0      0.2%  8.12Ki   __DATA,__bss
  0.2%  8.01Ki   0.2%  8.01Ki   [__DATA_CONST]
  0.2%  7.43Ki   0.2%  7.43Ki   Function Start Addresses
  0.0%    0      0.2%  6.88Ki   __DATA,__common

编译器优化

简单介绍一下最常见的几个可以减少二进制文件体积的编译优化选项。以下被提到的选项均在行业种被广泛使用。

  • -Os:之前介绍过,详见编译器工具链(三)——编译优化
  • -Wl,–strip-all(或者不添加-g选项):该选项告诉链接器删除调试部分。
  • -fno-unroll-loops:关闭循环展开,循环展开优化是一种常见的性能优化方法,会增加代码大小。
  • -fno-exceptions:从二进制文件中移除异常处理代码。
  • lto (-flto):通过-flto选项启用链接时优化会触发积极的编译器优化。许多函数和全局变量被优化掉,许多调用点会被去虚拟化。生成的二进制文件速度更快,同时也更小。但同时编译时长也会显著增长。

源码优化

除了编译器优化,通过软件的方式利用一些编程语言特性也可以缩减代码大小。

1. 代码重构

将函数定义挪到.c/.cpp文件。当函数定义放在头文件中时,它会在包含头文件的每个翻译单元中都拥有一份拷贝。即使只有一个定义,这些函数可能已经内联在它们的调用者中,这样,额外的代码会被保存在二进制文件中。因此,最好是将函数定义放在.c/.cpp文件中。
除了由开发人员编写的函数之外,还有编译器生成的函数,例如构造函数、析构函数、操作符重载等,这些函数也可能会因为类型结构或语言规则从而影响代码的大小。因此,程序员可以在.cpp文件中显式地实例化这些函数。例如,在test.h文件中,class被定义如下:

class A {
  A();
  A(A const&);
  ~A();
};

在test.cpp文件中,这些定义被实例化:

A::A() = default;
A(A const&) = default;
A::~A() = default;

与头文件中的函数定义类似的,模板函数也也会对代码大小产生影响。然而,要减少这些开销也不简单。通常情况下,一些类型会比其他类型使用得更加频繁,对于这种常用类型,我们可以在.cpp文件中显式地实例化它们。
例如,在test.h文件中,模板函数被定义如下:

template<class T>
struct  a {
void f(T t) { /* */ }
};

在test.cpp文件中,模板函数被显式实例化:

template struct A<int>;

显式实例化还可以节省编译时间,因为实例化只发生一次。

2. 函数属性

可以减少内联可能性的函数属性也可以帮助减少代码体积,例如:

__attribute__((cold))
__attribute__((noinline))

注意
在某些情况下,内联也有可能会减少代码体积。特别是对于小函数,内联删除了函数调用的开销,这有可能比函数体本身还要大。另外,建议有限地使用这些属性,因为它们可能会影响程序的可读性。

3. 从二进制中移出计算

当有了对于编译器优化和编程语言需求的良好知识储备,就有可能将计算从二进制文件中移出。一些表达式可以在编译时计算,而另一些表达式可以延迟到运行时再计算。这两种方法都有助于减小二进制大小。

  • 提前运算:使用c++的constexpr,static_assert等语言特性,一些表达式可以提前运算,例如:
constexpr auto gcd(int a, int b){
 while (b != 0){
  auto t = b;
  b = a % b;
  a = t;
 }
 return a;
}

int main(){
   int a = 11;
   int b = 121;
   int j = gcd(a,b);
   constexpr int i = gcd(10,12); // saves ‘2’ in the final assembly.
   return i + j;
}

使用 g++ -std=c++17 -fno-exceptions -S将上述程序编译:

main:
       mov     edx, 121
       mov     eax, 11
.L2:                 # inlined call to gcd(a, b)
       mov     ecx, edx
       cdq
       idiv    ecx
       mov     eax, ecx
       test    edx, edx
       jne     .L2
       add     eax, 2 # Precomputed value of gcd(10,12)
       ret

可以在汇编中看到,第二个gcd在编译时已经求值了,但是对gcd的第一个调用包含了完整代码,这是因为对gcd函数的第二次调用是一个constexpr。

移除死代码

几乎在任何大型程序中,都可能因为各种原因出现死代码。一些死代码可以用简单的技巧移除,例如

  • 查找产品中的测试和调试代码。nm工具可用于搜索二进制文件中的符号名称:
   nm <Binary> | grep -i "test\|debug"
  • 使用strings工具在二进制文件中查找字符串。之前讲过,strings可以打印所有在二进制中硬编码的C-strings。通过了解这些字符串,可以研究为什么某个特定的字符串会出现在二进制文件中。

References:

https://en.cppreference.com/w/cpp/language/constexp
https://learning.edx.org/course/course-v1:LinuxFoundationX+LFD113x+3T2021/block-v1:LinuxFoundationX+LFD113x+3T2021+type@sequential+block@a0bb2522e3de4eafaedaf26f44ac956e/block-v1:LinuxFoundationX+LFD113x+3T2021+type@vertical+block@1b3d0cfe5478439c8c694c7db3b35f2a

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值