- UID
- 1
- 精华
- 积分
- 76361
- 威望
- 点
- 宅币
- 个
- 贡献
- 次
- 宅之契约
- 份
- 最后登录
- 1970-1-1
- 在线时间
- 小时
|
前言
GCC的链接时间优化(LTO优化)是一个非常有效的优化选项,开启后,可以实现函数、常量跨编译单元内联等,真正实现C语言的高效性。
有多高效呢?参照以下帖子的实验,可以得出结论:开启LTO优化,可以把单片机的运行性能优化到几乎每一条指令都是关键指令,直接怼外设进行工作的。
【C】认识GCC的链接时间优化
https://www.0xaa55.com/thread-16842-1-1.html
【C】记一次GCC -O3优化效果实测
https://www.0xaa55.com/thread-16820-1-1.html
在使用STM32Cube之前,我使用的是STM32F1的“STM32F10x_StdPeriph_Driver”作为外设驱动的代码进行编程。由于编译环境是自己用gcc-arm-toolchain配合若干批命令搭的,单片机的启动代码(以及开头的中断表)是自己写的,在当时我似乎毫无障碍就可以直接开启LTO优化。
到后来我发现STM32CubeMX自动化生成代码很好用,建立的STM32CubeIDE工程也是用gcc编译的,我就开始使用STM32CubeIDE进行STM32单片机的开发了。不过经过几次折腾,我发现STM32CubeIDE的工程编译配置里,默认没有LTO优化的选项,并且当我试图在Miscellaneous里面手动添加-flto选项后,我发现它存在编译不通过的问题。
经过一段时间的上网搜索和调试,我发现了GCC7以前的LTO优化的Bug,这个Bug导致了编译上出现的各种符号相关的问题,但这些问题是可以不用更换编译器就能解决,并且能成功应用LTO优化。
初次尝试-flto命令参数
LTO优化的效果非常显著,在我看来,这是一个必开的优化选项。它对于C语言是关键的,因为它能真正实现跨编译单元优化,不仅可以减少大量的不必要的函数调度成本,而且很多仅框架性的代码能被直接优化得无影无踪,架构的设计变得更加自由高效了。
所以一开始我在工程编译属性里面,先切换到Release,设置-O3优化选项,然后就是关键的地方——尝试给gcc的命令行选项直接加上-flto,看看是不是直接就有效果。
结果编译失败了:说找不到函数_sbrk的符号。
这个sbrk是个什么东西呢?我大致搜索了一下发现它好像是*nix系统在底层用于管理内存的东西,相当于malloc()之类的实现,要靠这个sbrk()来完成。
但是我发现工程里其实已经实现了sbrk()这个函数了。是gcc的系统库要调用sbrk(),而STM32CubeMX生成的代码里,sbrk()的实现在sysmem.c里面完成。
搜了ST自己的论坛、github、stackoverflow后,发现这些网站上搜得到的东西给的方法都不管用。按照这些搜出来的内容,我尝试了很多的__attribute__()修饰,比如禁止函数被内联、把函数当成中断处理程序,或者弄个指针变量取函数指针等。然而都没用。
搜遍了英文的资料后我打算看看别的语言的资料会不会有靠谱的。我在谷歌上搜到了一个德语的论坛,里面似乎给出了方法——虽说我其实根本看不懂德语,但不知怎么的总之就看懂了一句:“得使用__attribute__((__used__))修饰来加上used属性”
我照着加上了以后,成功通过编译!看样子没有什么问题了,我直接插上ST-LINK把程序烧写进了单片机。
一直以来的疑惑,gcc开LTO的时候,如果要链接一个汇编程序.o进去会怎样?
这个疑惑我今天是揭晓了。被坑得目瞪口呆!先说结论:这个汇编程序会被孤立,它对C程序符号的导入会被链接器无视,因为gcc7链接器在进行LTO代码生成的时候,只考虑参与了LTO部分的函数的符号,很多函数在LTO编译期间就被直接去掉了,因为没有调用者。
首先,我把程序烧录进单片机后,拔掉烧录器,然后把单片机的USB线,插上电脑。
正常情况下(Debug的情况下),单片机会以USB CDC的方式报告,然后电脑就多了一个COM串口设备,我用VB程序可以直接读写串口控制我这个单片机。
但现在的情况是:单片机插电脑上,电脑和单片机都一点反应也没有。单片机本身GPIO C13管脚接了个LED,它启动后,这个LED会亮,但我发现它现在没亮。
这说明单片机可能根本就没有运行。我用STM32CubeIDE的按指令调试功能调试的话,发现好像指令也是照常跑的,没有出问题的样子:
但是一旦我想设置个断点,然后等它停留到断点上的时候,这个单片机就跑到了Default_Handler的死循环位置上了。我一度怀疑是看门狗被启用了,然后就是被狗咬了,但好像不是。我是在HAL_Delay()的位置里会进入这个死循环。
如果是在HAL_Delay()的地方,它突然跳到Default_Handler上,死循环了,那就说明应该是发生了一次中断,然后这个中断直接跳到Default_Handler上了。而HAL_Delay()里最容易发生的中断应该是SysTick的中断,但为何它没有进入SysTick_Handler()呢?我首先尝试在STM32CubeIDE里找到SysTick_Handler(),并且想要给它打断点。
然而!找不到!这个函数没了!
我顿时意识到,LTO会把不需要用到的函数去掉,这个SysTick_Handler()是不是被当作不需要的函数被去掉了呢?就像上述的sbrk()一样。但,我发现 __attribute__((__used__)) 对sbrk()的问题有效,对SysTick_Handler()无效,不管怎么加前缀,不管是extern还是used还是interrupt,都不能让这个函数出现在编译出来的bin里。
我打开了启动代码汇编文件 startup_stm32f103cbtx.s 想看个究竟。这个启动代码文件,确实是自己定义了一堆中断向量的弱符号,然后都转向Default_Handler()。按照推测,这个汇编的文件应该是无法参与LTO编译过程的,这些汇编文件的弱符号,在LTO优化期间也无法被C语言的强符号替用,因为LTO会把没有用到的函数去掉,而在被加入中断表之前,这些函数孤零零的,并没有其调用者,自然就会被去掉。
自己动手,编写C语言版本的启动代码,就像之前自己徒手搭建STM32编译环境一样
由于这个启动代码汇编挺简短的,我很轻易就写出来了。- #include<stdint.h>
- #include<stddef.h>
- #include<string.h>
- extern char _sidata;
- extern char _sdata;
- extern char _edata;
- extern char _sbss;
- extern char _ebss;
- extern char _estack;
- #define BootRAM (void*)0xF108F85F
- extern void SystemInit(void);
- extern void __libc_init_array(void);
- extern int main(void);
- __attribute__((section(".text.Reset_Handler")))
- static void CopyDataInit()
- {
- char *src = &_sidata;
- char *dst = &_sdata;
- size_t count = (size_t)(&_edata) - (size_t)(&_sdata);
- memcpy(dst, src, count);
- }
- __attribute__((section(".text.Reset_Handler")))
- static void FillZerobss()
- {
- char *dst = &_sbss;
- size_t count = (size_t)(&_ebss) - (size_t)(&_sbss);
- memset(dst, 0, count);
- }
- __attribute__((weak, section(".text.Reset_Handler")))
- void Reset_Handler(void)
- {
- CopyDataInit();
- FillZerobss();
- SystemInit();
- __libc_init_array();
- main();
- }
- __attribute__((section(".text.Default_Handler")))
- void Default_Handler()
- {
- for(;;);
- }
- __attribute__ ((weak, alias ("Default_Handler"))) void NMI_Handler();
- __attribute__ ((weak, alias ("Default_Handler"))) void HardFault_Handler();
- __attribute__ ((weak, alias ("Default_Handler"))) void MemManage_Handler();
- __attribute__ ((weak, alias ("Default_Handler"))) void BusFault_Handler();
- __attribute__ ((weak, alias ("Default_Handler"))) void UsageFault_Handler();
- __attribute__ ((weak, alias ("Default_Handler"))) void SVC_Handler();
- __attribute__ ((weak, alias ("Default_Handler"))) void DebugMon_Handler();
- __attribute__ ((weak, alias ("Default_Handler"))) void PendSV_Handler();
- __attribute__ ((weak, alias ("Default_Handler"))) void SysTick_Handler();
- __attribute__ ((weak, alias ("Default_Handler"))) void WWDG_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void PVD_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void TAMPER_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void RTC_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void FLASH_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void RCC_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void EXTI0_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void EXTI1_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void EXTI2_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void EXTI3_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void EXTI4_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel1_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel2_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel3_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel4_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel5_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel6_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel7_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void ADC1_2_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void USB_HP_CAN1_TX_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void USB_LP_CAN1_RX0_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void CAN1_RX1_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void CAN1_SCE_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void EXTI9_5_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void TIM1_BRK_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void TIM1_UP_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void TIM1_TRG_COM_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void TIM1_CC_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void TIM2_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void TIM3_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void TIM4_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void I2C1_EV_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void I2C1_ER_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void I2C2_EV_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void I2C2_ER_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void SPI1_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void SPI2_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void USART1_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void USART2_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void USART3_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void EXTI15_10_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void RTC_Alarm_IRQHandler();
- __attribute__ ((weak, alias ("Default_Handler"))) void USBWakeUp_IRQHandler();
- __attribute__((section(".isr_vector"), used))
- void *const g_pfnVectors[] =
- {
- &_estack,
- Reset_Handler,
- NMI_Handler,
- HardFault_Handler,
- MemManage_Handler,
- BusFault_Handler,
- UsageFault_Handler,
- 0,
- 0,
- 0,
- 0,
- SVC_Handler,
- DebugMon_Handler,
- 0,
- PendSV_Handler,
- SysTick_Handler,
- WWDG_IRQHandler,
- PVD_IRQHandler,
- TAMPER_IRQHandler,
- RTC_IRQHandler,
- FLASH_IRQHandler,
- RCC_IRQHandler,
- EXTI0_IRQHandler,
- EXTI1_IRQHandler,
- EXTI2_IRQHandler,
- EXTI3_IRQHandler,
- EXTI4_IRQHandler,
- DMA1_Channel1_IRQHandler,
- DMA1_Channel2_IRQHandler,
- DMA1_Channel3_IRQHandler,
- DMA1_Channel4_IRQHandler,
- DMA1_Channel5_IRQHandler,
- DMA1_Channel6_IRQHandler,
- DMA1_Channel7_IRQHandler,
- ADC1_2_IRQHandler,
- USB_HP_CAN1_TX_IRQHandler,
- USB_LP_CAN1_RX0_IRQHandler,
- CAN1_RX1_IRQHandler,
- CAN1_SCE_IRQHandler,
- EXTI9_5_IRQHandler,
- TIM1_BRK_IRQHandler,
- TIM1_UP_IRQHandler,
- TIM1_TRG_COM_IRQHandler,
- TIM1_CC_IRQHandler,
- TIM2_IRQHandler,
- TIM3_IRQHandler,
- TIM4_IRQHandler,
- I2C1_EV_IRQHandler,
- I2C1_ER_IRQHandler,
- I2C2_EV_IRQHandler,
- I2C2_ER_IRQHandler,
- SPI1_IRQHandler,
- SPI2_IRQHandler,
- USART1_IRQHandler,
- USART2_IRQHandler,
- USART3_IRQHandler,
- EXTI15_10_IRQHandler,
- RTC_Alarm_IRQHandler,
- USBWakeUp_IRQHandler,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- BootRAM
- };
复制代码 写完后,重新编译,一点问题也没有。烧录单片机,上电,单片机正常启动,功能全部上线。
这说明,汇编文件在工程里不仅不会参与LTO优化过程,而且很多时候是个累赘,还不如用C语言来写,并结合链接器脚本的行为把必须插入到bin里面的部分声明好。
结论
开启LTO优化后,由于gcc编译器不会把GAS汇编的语句内容也转换成LTO优化的特殊字节码,所以GAS汇编的部分是不会参与到LTO编译的过程的。
GAS汇编文件的导入符号,对于gcc7,是不起作用的。GAS汇编导入的C语言函数,因为这个C语言函数在LTO优化期间,要么被内联了,要么因为没有调用者,被移除了。gcc在链接时间生成的这块LTO相关的代码,就没有这个符号了。
对应之前的sbrk()函数找不到符号的问题,其实很有可能是gcc提供的最小系统库(提供诸如memcpy、sscanf、sprintf等函数)在编译的时候没有开启LTO选项,它没有生成对应的LTO段。而且事实上在IDA里也可以看出,像memcpy、memset这些函数,不能像别的C语言函数那样只要足够简短就能被内联——其实还有一点是gcc自带一个优化,比如,它会识别你自己用指针捏的内存拷贝,然后将其替换为对memcpy的调用——个人感觉这是一种负优化,因为memcpy在单片机上的实现其实就是按字节拷贝,根本不像在PC上那样高效。
反正,要开LTO优化,做这三件事:
- 工程选项设置gcc命令参数要有-flto
- sysmem.c的sbrk函数,加一个__attribute__((__used__))
- startup重写C
LTO优化的效果还是非常明显的,用IDA可以直接看出,开了LTO优化,HAL层几乎就只剩下那几个大头函数了。
开启LTO优化前:
开启LTO优化后:
|
|