这是个有趣的问题。实际上如果只是想把一个东西做得体积很大很简单,一个真实的故事:
一个算法有 2 个较为固定的参数,每个参数有 5 种可能性,那么如果我把这些参数静态编译进去就是 25 种可能性,如果再考虑到在 5 种不同硬件上的组合,那就是 125 种。如果假设每种的代码有 1KB 大,再有三四十个类似的算法,光这玩意就能整出 4MB 来。
类似地,如果工程中涉及到根据外部数据文件生成可执行代码,如 RPC 协议通信等,随着数据越来越多,附加的代码也会越来越多。
另外,编译器优化也可能导致代码体积的指数增长,常见地例如:
* 如果一个函数里面出现了对其他函数的多次独立调用,而这些函数又被 inline 。该函数实际生成的代码会包含子函数代码的多个拷贝。如果多个函数 inline 了同一个函数,该子函数代码会被在不同函数中多次拷贝。
* 如果一个函数被多次调用,并且调用时使用了常量参数,该函数的代码可能会被拷贝多份,并将常量参数代入其中单独优化。
* 循环的 unroll 会将循环体代码拷贝多份(虽然一般会有一定的长度限制)。
* 循环的向量化会将循环体代码拷贝多份(不同于 unroll ,这个是向量化逻辑所必须的,因此一般不会限制)。
我这有个对比的例子是 GTK3 。目前我这的 GTK3 包一共 49MB ,不过里面有一堆 GTK 元数据和 locale 之类的东西。只看最关键的
libgtk-3.so ,这玩意一共 7.8 MB ,其中 .text 大概有 3.7MB ,由 500 多个 .o 链接而成,这些 .o 的 .text 大小相加和
libgtk-3.so 的 .text 差不多。.text 最大的前 10% 的 .o ,.text 大小加起来大概 1.6MB 。这些 .o 对应的源码大小加起来大概 7.6MB 。总的源码大小是 17MB (只算 .c ,不算 header )。把 .o 的 .text 大小,和对应 .c 文件的大小画个图,可以很清楚地发现 .c 文件的大小大概是 .text 大小的 4-6 倍,平均值是 5 倍左右。
ffmpeg 包一共 37MB ,其中最大的是
libavcodec.so ,15MB 。.text 有 10MB 。大概有 1100 个 .o (我去除了一部分直接汇编出的 .o ,这些 .o 的 .text 加起来大概 1MB )。.text 最大的前 10% 的 .o ,.text 大小加起来大概 5.3MB 。这些 .o 对应的源码大小加起来大概 5.6MB ,总的源码大小是 19MB 。部分 .o 依然满足 4-6 倍的规律,但是整体的分布分散了许多。很多头部的 .o ,其 .c 文件大小是 .text 文件大小的两倍,一倍,甚至零点几倍。总的来说相比 GTK3 ,ffmpeg 编译生成的 .text 要更大。
进一步研究源码发现:
* libavcodec 源码中有很多以 _template.c 结尾的文件,加起来 1MB 左右,这些文件并不会直接编译出 .o (因此并不会计算在上面的 .c 大小上),而主要是被以不同的参数(如位深度)等多次 include 到不同的 .c 中,其实类似于模拟 C++ 的 non-type template parameter 。这些文件中的代码自然也会被拷贝多份。
* 部分源码大量使用了定义宏再多次以不同参数使用宏的技巧,这个还是类似于 non-type template parameter ,同样会导致同样的代码被拷贝多份。
* .o 中的代码存在大量 inline 的痕迹。也出现了一些 constprop 函数。源码中大量函数标记了 always_inline 。
#14 的链接中指出使用 --enable-small 选项(即指示编译器优先优化代码体积)可以数倍降低
libavcodec.so 的大小。暗示编译器优化是巨大体积的重要因素。不过我实测的效果并没有那么吼,.text 大概降低了 40% 左右。相对地,
libgtk-3.so 降低了不到 20%。不过确实可以看到很多的 .o 文件 .text 大小成倍地缩小。GTK3 则鲜有缩小超过 30% 的 .o 。