查看: 846|回复: 3

[原创] 使用SEGGER链接器如何压缩更多代码?

[复制链接]
  • TA的每日心情
    开心
    2023-6-12 14:34
  • 签到天数: 165 天

    连续签到: 1 天

    [LV.7]常住居民III

    发表于 2021-7-19 15:16:12 | 显示全部楼层 |阅读模式
    分享到:


    背景
    Segger链接器对于RISC-V处理器的优化效果:
    架构
    链接器
    R/O代码
    R/O数据
    R/O
    RV32IMAC
    GNU
    GNU
    43176
    4608
    47784
    RV32IMAC
    GNU
    SEGGER
    37772
    3623
    41395
    RV32IMAC
    GNU
    SEGGER + outlining
    36086
    3623
    39709
      应用程序通常依据逻辑功能被分为多个源文件进行管理。在开发过程中,这样做的好处是,当更改单个C源文件时,不需要重新编译整个应用程序。

    单个文件编译的副作用是优化通常不能跨过编译单元的边界。你可以打开 SEGGER工具集支持的链接时优化选项(LTO),但该方式仅适用于源代码模块。如果应用程序含有由芯片供应商提供的第三方目标代码库,则不能通过链接时优化选项优化代码!

    但在LTO之外,你仍然有机会减少应用程序的大小。
    特征法(Outlining)

    减小应用程序大小的方法之一是搜索函数之间、甚至函数内部的通用指令序列。一旦确定,就可以将公共序列提取到子例程中,使用子程序调用来替换该指令序列。

    因为子程序替换在机器码层级,而不是高级语言层级上运行的,因此提取的子程序不需要遵守任何ABI强制的调用约定。这意味着搜索的范围可以更广,可以将多条指令,从短到长作为可用作替换的候选指令序列。

    由于SEGGER 链接器的特征优化可与任何目标代码(包括第三方目标代码库)一起使用,并且适用于整个应用程序,因此大大拓宽了发现特征序列机会。

    SEGGER 链接器会仔细分析应用程序,选择缩减代码体积的最佳方法:采用最长的序列可能不是一个好的策略,而采用较短的序列通常会更好。

    示例

    这是来自emWin应用程序的真实示例。在产品使用寿命期间内,由于采用的LCD面板缺货,需要采用另一种兼容性较高的面板进行替换。但是,固件需同时支持两种面板产品。为此,固件会在运行时检测安装的面板,并使用合适的面板驱动程序初始化emWin。最终,应用程序包含两个驱动,但应用层代码不需知道实际使用的面板,但是能够在新旧硬件上同样良好地运行。

    面板驱动程序非常相似。两个驱动程序之间有一定数量的重复代码。下面是示例代码,其中的差异用红色突出显示:

    ReadRectCust_16bpp:        ReadRectCust_18bpp:
    ADDI sp,sp,-32                 ADDI sp,sp,-32
    SW s2,16 (sp)                  SW s2,16(sp)
    LW s2,8(a0)                     LW s2,8(a0)
    SW s0,24 (sp)                  SW s0、24(sp)
    SW s1、20(sp)                   SW s1、20(sp)
    LW a6、168(s2)                 LW a6、168(s2)
    SW s3、12(sp)                   SW s3、12( sp)
    SW s4、8(sp)                     SW s4、8(sp)
    SW s5、4(sp)                     SW s5、4(sp)
    SW ra,28(sp)                    SW ra,28(sp)
    MV a0,s2                             MV a0 ,s2
    MV s5,a1                             MV s5,a1
    MV s4,a2                             MV s4,a2
    MV s1,a3                             MV s1,a3
    MV s0,a4                             MV s0,a4
    MV s3,a5                             MV s3,a5
    JALR a6                                 JALR a6
    SUB a2,s1,s5                      SUB a2,s1,s5
    SUB s0,s0,s4                      SUB s0,s0,s4
    ADDI s0,s0,1                      ADDI s0,s0, 1
    ADDI a2 ,a2,1                    ADDI a2,a2,1
    MUL a2,a2,s0                     MUL a2,a2,s0
    LW a4,64(s2)                   LW a4,64(s2)
    LW s0,24(sp)                 LW s0,24(sp)
    LW t1,228(s2)                 LW t1,232(s2)
    LW ra,28(sp)                    LW ra,28(sp)
    LW s1,20(sp)                    LW s1,20(sp)
    LW s2,16(sp)                    LW s2、16(sp)
    LW s4、8(sp)                      LW s4、8(sp)
    LW s5、4(sp)                      LW s5、4(sp)
    MV a1,s3                             MV a1,s3

    LW a0,24(a4)                  LW a0,24(a4)
    LW s3,12(sp)                    LW s3,12(sp)
    ADDI sp,sp,32                    ADDI sp,sp,32
    JR t1                                      JR t1

    这两个函数之间似乎有很多重复。我们将基于此代码进行优化解析。
    RISC-V相关指令

    不熟悉RISC-V指令的人员可以阅读下面的内容:

    回复

    使用道具 举报

  • TA的每日心情
    开心
    2023-6-12 14:34
  • 签到天数: 165 天

    连续签到: 1 天

    [LV.7]常住居民III

     楼主| 发表于 2021-7-19 15:23:14 | 显示全部楼层
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2023-6-12 14:34
  • 签到天数: 165 天

    连续签到: 1 天

    [LV.7]常住居民III

     楼主| 发表于 2021-7-19 15:23:44 | 显示全部楼层
    不熟悉RISC-V指令的人员可以阅读下面的内容:

    JAL label指令实现对子例程的调用,将返回地址放入ra寄存器。这与ARM 的BL指令相同,后者将返回地址放入lr寄存器。

    JALR reg是一个子程序调用,调用地址保存在寄存器reg中,将返回地址放到寄存器ra。这与ARM指令BLX reg相同,后者将返回地址放入lr寄存器。

    JAL reg,label是对label子例程的调用,将返回地址放入reg中。没有与之等效的ARM指令。

    JR reg跳转到寄存器reg中保存地址。与ARM指令BX reg相同, 用于从RISC-V和ARM中函数的返回,跳转至ra或lr寄存器中保存的地址。
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2023-6-12 14:34
  • 签到天数: 165 天

    连续签到: 1 天

    [LV.7]常住居民III

     楼主| 发表于 2021-7-19 15:24:09 | 显示全部楼层
    如何执行优化

    SEGGER链接器在多个函数之间找到通用性,并使用子程序重写这些函数。上面的两个函数转换为具有相同功能的以下内容:

          

    ReadRectCust_18bpp:             _ReadRectCust_16bpp:

    JAL tp,<outline.62.38>           JAL tp,<outline.62.38>

    JALR a6                                   JALR a6

    JAL tp,<outline.144.24>         JAL tp,<outline.144.24>

    LW t1,232 (s2)                    LW t1 228(s2)

    J <tail.52.20>                           J <tail.52.20>



           这确实节省了很多代码空间!



    函数进入时,将调用具有一个通用序列<outline.62.38>的子例程。该标签中的第一个数字是特征序列的序列号:在本例中,它是最终应用程序中的第62个特征序列。第二个数字表示特征序列中包含多少个指令字节:在本例中是38个字节。



    特征序列的调用不使用RISC-V的标准返回寄存器,而是换成了替代寄存器tp,特征序列使用该寄存器返回:

    <outline.62.38>:

      0x08009F38 1101     ADDI sp,sp,-32

      0x08009F3A C84A    SW s2,16(sp)

      ...

      0x08009F5A 89BE    MV s3,a5

      0x08009F5C 8202     JR tp



          链接器选择使用tp,无需破坏标准返回地址寄存器(ra)的值。链接器可以将tp寄存器作为备用返回寄存器,而不改变其任何功能。

    通过a6进行调用之后是第二个特征序列<outline.144.24>。此后,将保留两个驱动之间不同的那条指令,然后执行下一个特征序列。

    更好的可能性!

    为什么特征序列<outline.144.24>没有封装JALR a6指令?

    原因是,通过a6调用的函数本身可能会受到特征序列的约束,并且很可能破坏tp中保存的值。如果将JALR a6指令移至特征序列中,则通过a6进行的调用可能会破坏tp中保存的特征函数的返回地址,因此导致无效的转换。

    结论

    这是只有一个特征序列的示例。在搜索特征序列时,可以调整链接器的特征参数,设置特征序列的最小和最大长度。即使是在体积优化的代码中,链接器也可以找到许多的特征序列。

    如果代码体积对你很重要,则SEGGER 链接器可以用多种方式减小你的应用程序映像。
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /2 下一条

    手机版|小黑屋|与非网

    GMT+8, 2024-4-25 23:12 , Processed in 0.138340 second(s), 21 queries , MemCache On.

    ICP经营许可证 苏B2-20140176  苏ICP备14012660号-2   苏州灵动帧格网络科技有限公司 版权所有.

    苏公网安备 32059002001037号

    Powered by Discuz! X3.4

    Copyright © 2001-2024, Tencent Cloud.