Pat*_*ght 29 c++ gcc arm code-size compiler-optimization
我正在使用 GCC 为 ARM 开发 C++。我遇到了一个问题,我没有启用优化,我无法为我的代码创建二进制文件(ELF),因为它不适合可用空间。但是,如果我只是启用调试优化(-Og)(据我所知这是可用的最低优化),代码就很容易适应。
在这两种情况下,都会启用-ffunction-sections、-fdata-sections、-fno-exceptions和-Wl,--gc-sections 。
即使进行了最小的优化,二进制大小也存在巨大差异。
我查看了3.11 控制优化的选项,详细了解使用 -Og 标志执行哪些优化,看看这是否会给我任何见解。
哪些优化标志对二进制大小影响最大?我应该寻找什么来解释这种巨大的差异吗?
eer*_*ika 26
哪些 GCC 优化标志对二进制大小影响最大?
根据程序本身的不同,它会有所不同。要了解每个标志如何影响您的程序,最准确的方法是尝试并将结果与基本级别进行比较。
大小优化的基本级别的一个不错选择是使用 -Os,它可以实现 -O2 的所有优化,除了那些预计可能显着增加二进制大小的优化(目前):
-falign-functions
-falign-jumps
-falign-labels
-falign-loops
-fprefetch-loop-arrays
-freorder-blocks-algorithm=stc
Run Code Online (Sandbox Code Playgroud)
Pet*_*des 18
未优化构建的大部分额外代码大小是这样一个事实:默认值也意味着调试构建,即使您使用 GDB命令跳转到不同的源代码-O0
行,也不会跨语句在寄存器中保留任何内容以实现一致的调试。j
相同的功能。 -O0
与最轻的优化相比,这意味着大量的存储/重新加载,对于无法使用内存源操作数的非 CISC ISA 上的代码大小来说尤其是灾难性的。 为什么 clang 使用 -O0 生成低效的 asm(对于这个简单的浮点和)?同样适用于 GCC。
特别是对于现代 C++,调试构建是灾难性的,因为简单的模板包装函数通常在简单情况下(或者可能是一条指令)内联并优化为无任何内容,而是编译为必须设置参数并运行调用指令的实际函数调用。例如,对于 a std::vector
,operator[]
成员函数通常可以内联到单个ldr
指令,假设编译器将.data()
指针存储在寄存器中。但如果没有内联,每个调用站点都会接受多个指令1
对实际第1.text
节中的代码大小影响最大的选项:一般分支目标的对齐,或只是循环,会花费一些代码大小。除此之外:
-ftree-vectorize
- 使 SIMD 版本循环,如果编译器无法证明迭代计数将是向量宽度的倍数,则还需要进行标量清理。(或者,如果您不使用 ,则指向的数组是不重叠的restrict
;这可能还需要标量回退)。在 GCC11 及更早版本中启用-O3
。在 GCC12 及更高版本中启用-O2
,如 clang。
-funroll-loops
/-funroll-all-loops
- 即使在现代 GCC 中也默认不启用-O3
。通过配置文件引导优化 ( -fprofile-use
) 启用,当它具有来自构建的分析数据时,-fprofile-generate
可以了解哪些循环实际上很热门并且值得花费代码大小。(而且它们是冷的,因此应该针对大小进行优化,以便在它们运行时减少 I-cache 未命中,并减少其他代码的驱逐。) PGO 也会影响矢量化决策。
与循环展开相关的是控制循环剥离(完全展开)以及展开程度的启发式(调节旋钮)。设置这些的正常方法是 with -march=native
,也表示-mtune=
“whatever” 。 与或-mtune=znver3
相比,可能有利于大的展开因素(至少 clang 如此)。但是有 GCC 选项可以手动调整个别内容,如gcc 的评论中所讨论的:为简单循环生成的奇怪的 asm和如何要求 GCC 完全展开此循环(即,剥离此循环)?
也有一些选项可以覆盖其他决策启发式的权重和阈值,例如内联,但您很少需要微调那么多,除非您正在努力完善默认值,或者为新 CPU 找到良好的默认值。-mtune=sandybridge
-mtune=haswell
-Os
- 优化大小和速度,尽量不要牺牲太多速度。如果您的代码有很多 I-cache 未命中,这是一个很好的权衡,否则-O3
通常会更快,或者至少这是 GCC 的设计目标。值得尝试不同的选项,看看您的代码是否比-O2
您关心的某些 CPU 更快;有时,某些微架构的错过优化或怪癖会产生影响,例如如果我针对大小而不是速度进行优化,为什么 GCC 会生成速度快 15-20% 的代码?它具有从 GCC4.6 到 4.8(当时当前)的实际基准测试,针对测试程序中的特定小循环,在相当多不同的 x86 和 ARM CPU 上,无论是否进行实际调整。 但是,没有理由期望它能够代表其他代码,因此您需要针对自己的代码库进行测试。(对于任何给定的循环,小的代码更改可以使不同的编译选项在任何给定的 CPU 上更好。)-Os
-O3
-march=native
-Os
如果您需要较小的静态代码大小以适应某些大小限制,显然这是非常有用的。
-Oz
仅针对大小进行优化,即使在速度上付出很大代价。GCC 最近才将其添加到当前主干中,因此预计会在 GCC12 或 13 中出现。大概我在下面写的关于 clang 的-Oz
相当激进的实现也适用于 GCC,但我还没有测试它。
Clang 有类似的选项,包括-Os
. 它还可以选择clang -Oz
仅针对大小进行优化,而不关心速度。它非常激进,例如在 x86 上使用代码高尔夫技巧,例如 push 1; pop rax
(总共 3 字节)而不是mov eax, 1
(5 字节)。
不幸的是, GCC-Os
选择使用div
乘法逆元来除以常数,这会降低速度,但不会节省太多大小(如果有的话)。(对于 x86-64 ,https://godbolt.org/z/x9h4vx1YG)。-Os
对于 ARM,如果您不使用-mcpu=
暗示udiv
甚至可用的a, GCC仍然使用逆,否则它使用udiv
: https: //godbolt.org/z/f4sa9Wqcj。
Clang-Os
仍然使用乘法逆元 with umull
,仅使用udiv
with -Oz
。(或者__aeabi_uidiv
不带任何-mcpu
选项调用辅助函数)。因此,在这方面,clang -Os
它比 GCC 做出了更好的权衡,仍然花费一点代码大小来避免缓慢的整数除法。
std::vector
#include <vector>
int foo(std::vector<int> &v) {
return v[0] + v[1];
}
Run Code Online (Sandbox Code Playgroud)
Godbolt与gcc
默认值-O0
vs.-Os
只是-mcpu=cortex-m7
随机选择一些东西。std::vector
IDK 如果像在实际的微控制器上一样使用动态容器是正常的可能不会。
# -Os (same as -Og for this case, actually, omitting the frame pointer for this leaf function)
foo(std::vector<int, std::allocator<int> >&):
ldr r3, [r0] @ load the _M_start member of the reference arg
ldrd r0, r3, [r3] @ load a pair of words (v[0..1]) from there into r0 and r3
add r0, r0, r3 @ add them into the return-value register
bx lr
Run Code Online (Sandbox Code Playgroud)
与调试构建(为 asm 启用名称解析)
# GCC -O0 -mcpu=cortex-m7 -mthumb
foo(std::vector<int, std::allocator<int> >&):
push {r4, r7, lr} @ non-leaf function requires saving LR (the return address) as well as some call-preserved registers
sub sp, sp, #12
add r7, sp, #0 @ Use r7 as a frame pointer. -O0 defaults to -fno-omit-frame-pointer
str r0, [r7, #4] @ spill the incoming register arg to the stack
movs r1, #0 @ 2nd arg for operator[]
ldr r0, [r7, #4] @ reload the pointer to the control block as the first arg
bl std::vector<int, std::allocator<int> >::operator[](unsigned int)
mov r3, r0 @ useless copy, but hey we told GCC not to spend any time optimizing.
ldr r4, [r3] @ deref the reference (pointer) it returned, into a call-preserved register that will survive across the next call
movs r1, #1 @ arg for the v[1] operator[]
ldr r0, [r7, #4]
bl std::vector<int, std::allocator<int> >::operator[](unsigned int)
mov r3, r0
ldr r3, [r3] @ deref the returned reference
add r3, r3, r4 @ v[1] + v[0]
mov r0, r3 @ and copy into the return value reg because GCC didn't bother to add into it directly
adds r7, r7, #12 @ tear down the stack frame
mov sp, r7
pop {r4, r7, pc} @ and return by popping saved-LR into PC
@ and there's an actual implementation of the operator[] function
@ it's 15 instructions long.
@ But only one instance of this is needed for each type your program uses (vector<int>, vector<char*>, vector<my_foo>, etc.)
@ so it doesn't add up as much as each call-site
std::vector<int, std::allocator<int> >::operator[](unsigned int):
push {r7}
sub sp, sp, #12
...
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,未经优化的 GCC 更关心快速编译时间,甚至比最简单的事情(例如避免mov reg,reg
在计算一个表达式的代码中无用的指令)更关心。
如果您可以使用元数据编写整个 ELF 可执行文件,而不仅仅是需要刻录到闪存的 .text + .rodata + .data,那么-g
调试信息当然对于文件大小非常重要,但基本上无关紧要,因为它不是与运行时所需的部分混合在一起,因此它只是位于磁盘上。
符号名称和调试信息可以用gcc -s
或删除strip
。
堆栈展开信息是代码大小和元数据之间的有趣权衡。 -fno-omit-frame-pointer
浪费额外的指令和寄存器作为帧指针,导致机器代码大小更大,但.eh_frame
堆栈展开元数据更小。(strip
默认情况下不考虑“调试”信息,即使对于非 C++ 的 C 程序,在非调试上下文中异常处理可能需要它。)
如何从 GCC/clang 汇编输出中消除“噪音”?提到如何让编译器省略其中的一些内容:-fno-asynchronous-unwind-tables
省略.cfi
asm 输出中的指令,从而省略进入该.eh_frame
部分的元数据。另外-fno-exceptions -fno-rtti
用C++可以减少元数据。(反射的运行时类型信息占用空间。)
控制节/ ELF 段对齐的链接器选项也可能占用额外的空间,与微小的可执行文件相关,但基本上是恒定的空间量,不随程序的大小而缩放。另请参阅对于小型程序,链接后最小可执行文件大小现在比 2 年前大 10 倍?
归档时间: |
|
查看次数: |
9484 次 |
最近记录: |