华为 您所在的位置:网站首页 fpic是编译选项还是链接选项 华为

华为

2023-06-04 21:09| 来源: 网络整理| 查看: 265

作者: 周紫鹏 / 后期编辑:张汉东

背景介绍

Rust编译后的可执行文件大小一直是大家谈论比较多的问题,对于嵌入式单板空间有限的场景下,太大的可执行文件往往是不可接受的。当前的项目也经常会因为几K的可执行文件增大而进行优化。

本篇文章对比Rust和C语言可执行文件大小和组成,并尝试提供一些有效的优化方式。Rust选择的是Tokio v1.5.0作为测试对象,C语言则选择公司内部某项目组模块作为测试对象。

Rust生成二进制类型介绍

Rust支持生成多种格式的动态库和静态库,在Cargo.toml文件中,新增[lib]段指定crate-type就可以进行配置。

[lib] crate-type = ["dylib"] 复制代码

[crate_type = "bin"]

生成可执行文件,crate中必须要有main函数作为入口,如果crate中已经有main函数,其实不需要在toml文件中显示指定。生成的可执行文件中,会包含所有Rust相关的库和依赖。也就是生成的可执行文件可以在没有安装Rust环境的机器上运行。

[crate_type = "lib"]

生成一个Rust库,但是具体的形态会根据不同的编译器来生成对应的lib库,生成的库是给rustc使用的,所以这个库的形式也会跟着rustc的变化而变化。

[crate_type = "dylib"]

生成一个动态的Rust库(Linux 上为 .so,MacOS 上为 .dylib, Windows 上为 .dll),生成的动态库可以作为其他库或者可执行文件的依赖库。该动态库会包含Rust的一些特定段,如.rustc等。

[crate_type = "staticlib"]

生成一个静态库(Linux\MacOS 上为 .a,Windows 上为 .lib),Rust编译器不会链接staticlib生成的静态库,因为该静态库会包含Rust库和依赖的第三方库,一般适合作为独立的Rust库实现提供给第三方,和bin的区别是,没有携带main函数。

[crate_type = "cdylib"]

C类型的动态库,与 dylib 类似,也会生成 .so, .dylib 或 .dll 文件,但是生成的为C-ABI格式的二进制,可以提供给C语言作为FFI调用。

[crate_type = "rlib"]

Rust lib文件,由于当前Rust的二进制格式是不稳定的,所以当前Rust还是使用源码集成一起编译的方式来进行构建,当前没有办法通过Cargo.toml的方式依赖编译好的SO、*.rlb或者.a。rlib作为Rust编译生成的中间二进制文件,会携带很多Rust语言相关的信息,最终是作为rustc的输入。在编译的过程中,可以在target\release\deps下看到依赖的三方库被编译成rlib。

[crate_type = "proc-macro"]

不会产生特定类型的库文件,Rust过程宏使用需要独立的crate,其他库通过依赖指定的proc-macro库进行使用。

本次分析主要以dylib库方式进行,避免引入第三方库依赖的影响。

可执行文件组成

Tokio v1.5.0中tokio模块的代码(NBNC)有36,473行,使用tokei工具进行统计的结果。

在tokio\tokio\Cargo.toml文件中添加crate-type = ["dylib"],指定编译结果为动态库形式。

使用cargo build --release编译

生成的libtokio.so大小为5,385,736字节,每个段的分布如下。第二列为段名称,第三列为段大小,最后一列为每千行代码包含的二进制大小。段的大小单位都为字节。

[Nr]Section NameSection SizeSection Size / KLOC[ 1].hash12,496347[ 2].gnu.hash12,928359[ 3].dynsym50,3761,399[ 4].dynstr194,0405,390[ 5].gnu.version4,198117[ 6].gnu.version_r2567[ 7].rela.dyn59,6161,656[ 8].rela.plt481[ 9].init261[10].plt481[11].plt.got160[12].text689,51719,153[13].fini90[14].rodata31,222867[15].eh_frame_hdr38,6601,074[16].eh_frame179,8684,996[17].gcc_except_table28,468791[18].tdata562[19].tbss2116[20].init_array80[21].fini_array80[22].data.rel.ro31,304870[23].dynamic57616[24].got5,008139[25].data1685[26].bss1604[27].comment170[28].rustc3,318,06092,168[29].debug_aranges1284[30].debug_info682[31].debug_abbrev361[32].debug_line1975[33].debug_str1073[34].debug_ranges1284[35].symtab185,5925,155[36].strtab539,04714,974[37].shstrtab34210

从表格中可以看到,release中仍然存在调试相关信息,包括符号表信息。针对调测信息,我们对SO进一步进行strip。

strip

strip命令可以将29到37的调测信息段删除,删除之后的libtokio.so大小为4,659,816,仍有4.5M左右的大小。

.rustc段

.rustc段大概占了整体大小的60%,关于.rustc段的作用是这样的,由于动态库dylib采用Rust ABI,目前这个ABI尚不稳定,需要.rustc这一节来附加额外的版本控制信息,在最终的可执行文件中不会存在rustc段。可以通过strip libtokio.so -R .rustc将.rustc段删除,删除之后的大小为1,341,680大小为1.3M 左右。

各段占比以及和C的对比

tokio数据C语言数据序号段段大小每千行大小百分比百分比每千行大小段[ 1].hash12,4963470.93%1.94%270.hash[ 2].gnu.hash12,9283590.96%2.25%313.gnu.hash[ 3].dynsym50,3761,3993.75%7.26%1,010.dynsym[ 4].dynstr194,0405,39014.46%6.01%836.dynstr[ 7].rela.dyn59,6161,6564.44%5.53%769.rela.dyn[12].text689,51719,15351.39%50.09%6,965.text[14].rodata31,2228672.33%8.34%1,159.rodata[15].eh_frame_hdr38,6601,0742.88%1.87%261.eh_frame_hdr[16].eh_frame179,8684,99613.41%10.05%1,397.eh_frame[17].gcc_except_table28,4687912.12%[22].data.rel.ro31,3048702.33%0.01%2.data.rel.ro[24].got5,0081390.37%1.56%217.got3704213,198

表格中C采用-O2优化等级,并通过strip之后的数据。按照经验值来看,每千行C代码编译出的二进制大小大概在13K左右。从表格对比来看,Rust编译出来的可执行文件大概是C语言的3倍。最主要增大点在.text段和.dynstr段。其中tokio比C多了.gcc_except_table段,该段和try-catch-finally 控制流块的异常相关,部分信息用于处理异常,其他信息用于清除代码(即:在展开堆栈时调用对象析构函数)。

.dynsym

这一节存储的是关于动态链接的符号表,每一个表项占24字节,tokio总共有2099个动态符号,相比较于C,Rust会存在更多的库函数、数据结构和异常处理等。

//Rust dynsym符号表 Symbol table '.dynsym' contains 2099 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZN3std3net3tcp9TcpStream 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZN3std2fs8DirEntry9file_ 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZN4core3fmt3num53_$LT$im 4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZN4core3fmt3num53_$LT$im 5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZN51_$LT$$RF$std..fs..Fi 6: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND _ZN3std10std_detect6detec 7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZN3std3sys4unix6thread6T 8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZN4core3fmt3num52_$LT$im 9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pipe2@GLIBC_2.9 (2) 10: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZN91_$LT$std..io..cursor 11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZN74_$LT$std..fs..DirEnt 12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZN4core6option13expect_f 13: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZN3std3net4addr12SocketA 14: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZN3std4path4Path5_join17 15: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZN4core3fmt3num53_$LT$im .... //C dynsym符号表 Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free@GLIBC_2.2.5 (2) 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __isoc99_fscanf@GLIBC_2.7 (3) 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2) 4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND clock_gettime@GLIBC_2.17 (4) 5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fclose@GLIBC_2.2.5 (2) 6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2) 7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __assert_fail@GLIBC_2.2.5 (2) 8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2) 9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND feof@GLIBC_2.2.5 (2) 10: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcpy@GLIBC_2.14 (5) 12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND malloc@GLIBC_2.2.5 (2) 13: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fopen@GLIBC_2.2.5 (2) 复制代码

.dynstr

dynstr段用来存储dysym符号表中的符号,本次测试使用的是rustc 1.48.0,组名规则为legacy,类似于C++的组名规则,符号名中间会加上crate、mod、struct等信息,想比于C语言的组名要大很多。

当前nightly版本支持了新的组名规则,V0规则,新的规则会删除符号最后的哈希值,但是组名之后的符号仍然是很长的。

//Rust 字符串表 String dump of section '.dynstr': [ 1] libstd-f14aca24435a5414.so [ 1c] _ITM_deregisterTMCloneTable [ 38] __gmon_start__ [ 47] _Jv_RegisterClasses [ 5b] _ITM_registerTMCloneTable [ 75] _ZN58_$LT$std..io..error..Error$u20$as$u20$core..fmt..Debug$GT$3fmt17heb882e9e5723aaeaE [ cd] _ZN244_$LT$std..error..$LT$impl$u20$core..convert..From$LT$alloc..string..String$GT$$u20$for$u20$alloc..boxed..Box$LT$dyn$u20$std..error..Error$u2b$core..marker..Send$u2b$core..marker..Sync$GT$$GT$..from..StringError$u20$as$u20$core..fmt..Display$GT$3fmt17h0381a183d16c0bdbE [ 1e0] _ZN3std2rt19lang_start_internal17h73711f37ecfcb277E [ 214] _ZN56_$LT$std..io..Guard$u20$as$u20$core..ops..drop..Drop$GT$4drop17h17ecb6f4aa594fe8E [ 26b] _ZN4core6result13unwrap_failed17he7cdc7a46f93cfbeE [ 29e] _ZN3std2fs11OpenOptions4read17hb9e61755aa4c5dd0E //C 字符串表 String dump of section '.dynstr': [ 1] libc.so.6 [ b] fopen [ 11] puts [ 16] __assert_fail [ 24] printf [ 2b] feof [ 30] __isoc99_fscanf [ 40] memcpy [ 47] fclose [ 4e] malloc [ 55] clock_gettime 复制代码

.text段

最后再打开看看最大头的代码段。.text段大概也是C的三倍左右大小,通过汇编指令打开查看,Rust比C多出点在异常处理、调用栈、析构函数、泛型实例化、Vec,Result,Box,String,Map等结构的处理、运行时边界校验等。

优化方式

上述我们只是用cargo build --release的方式进行了代码的优化,当然Rust编译器还提供了不同的优化手段。本节还是基于tokio,介绍常用的二进制优化手段。

优化手段二进制大小(字节)debug模式编译22,287,016release模式编译5,385,736strip之后大小4,659,816strip libtokio.so -R .rustc1,341,680codegen-units = 11,046,768panic = 'abort'未测试Optimize libstd with Xargo未测试

cargo支持的性能和二进制大小优化选项可以参见这里。

codegen-units

其中codegen-units = 1优化效果比较明显。该选项用来将crate分割成多个代码生成单元,当生成多个代码单元时,LLVM会并行的来处理,减少编译的时间。如果将codegen-units设置为1的时候,可以提升代码的运行速度,和减少生成的可执行文件,但是会大大增加编译的时间开销。在仅使用release时tokio编译时间为25s,在设置codegen-units = 1的时候,编译时间为39s,大概增加了**60%**的时间。默认情况下全量编译设置的值为16,增量编译下设置的值为256。

min-sized-rust

该仓中介绍了几种常用的优化方式,但是尝试使用opt-level = 'z'和lto = true两个选型对tokio最终生成的二进制并没有影响,当然这两个选项对性能有一定的提升。

Jemalloc在1.32版本已经被删除。

panic = 'abort'添加之后编译失败,正常Rust在panic的时候,会记录调用栈,如果改为panic='abort'之后,将会直接退出,而不会打印异常信息。

其他优化手段,如重新编译libstd、#![no_std]不使用标准库,也没有在本次测试范围内。

结论

Rust由于其组名规则和语言特性等原因,在使用了各种优化之后,编译出来的二进制大小大概是C语言的三倍左右,主要增大在代码段和动态符号表上。但是Rust语言比C的表达能力更强,同样的功能下,可以使用更少于C的代码量来实现,所以其二进制的增大还是可以接受。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有