Post

KN Cinterop静态库Codesize优化

介绍 KN 通过 cinterop 引入静态库时如何避免死代码进入最终 so:用 --exclude-libs 不导出静态库符号,并用 -ffunction-sections/-fdata-sections 编译静态库以配合 --gc-sections 做链接期死代码删除。

KN Cinterop静态库Codesize优化

KN项目可以通过cinterop引入动态/静态库,引入的静态库会被链接进KN的产物so中,链接时能做的codesize优化比较有限,可能引入一些死代码。如果cinterop引入的静态库只提供给KN调用,可以使用不导出+gc-section的方式删除死代码。

demo项目

https://github.com/linhandev/kn_samples/tree/static-cinterop-codesize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kn_samples/
├── add/                         # 带静态库 cinterop 的 Kotlin 库
│   ├── build.gradle.kts         # ohosArm64、cinterop add 静态库,发布到 maven-repo
│   └── src/
│       ├── nativeInterop/add/
│       │   ├── add.h
│       │   ├── add.c            # 功能实现
│       │   ├── add.def          # cinterop 定义
│       │   └── libadd.a         # 构建脚本中用 clang 从 add.c 打出的静态库
│       └── nativeMain/kotlin/
│           └── PlaceHolder.kt   # 占位,否则构建配置写起来很麻烦
│
├── src/nativeMain/kotlin/       # 出 so 的 KN 工程,调用 add 里的 cinterop 库,@CName 导出接口给 c 调用
│   └── Add.kt                   # @CName("add_c_name"),内部调用 add 的 addcfun
│
├── c-caller/                    # C 驱动,调上面 KN 的 so
│   └── main.c                   # 链接 libc2k.so,调用 add_c_name()
│
├── maven-repo/                  # 仓库内 Maven(add 的 klib 发布到这方便查看)
├── build.gradle.kts             # 根工程:ohosArm64,依赖 add,产出 libc2k.so
├── settings.gradle.kts          # include("add"),maven-repo 放在仓库首位
└── run.sh                       # 构建静态库 → 发布 add klib → 构建 c2k → 构建 C 驱动 → 部署

先看看静态库怎么进的KN产物

  1. 打出 libadd.a
  2. cinterop def配置
    1
    2
    3
    4
    
     package = add
     headers = add.h
     staticLibraries = libadd.a
     libraryPaths = src/nativeInterop/add
    
  3. 打出klib
    1. 可以看到静态库文件被包进去了
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      
        ./kn_samples/maven-repo/com/example/add-ohosarm64/1.0-SNAPSHOT/default
      ├── linkdata
      │   ├── module
      │   └── package_add
      │       └── 0_add.knm
      ├── manifest
      ├── resources
      └── targets
          └── ohos_arm64
              ├── included
              │   └── libadd.a
              ├── kotlin
              └── native
                  └── cstubs.bc
      
    2. manifest中有 staticLibraries=libadd.a ,应该是根据这个在出kn的so时加的链接 libadd.a 选项
  4. 引入这个klib依赖,kotlin代码中调用,打出kn的so。最终的链接选项中链入了 libadd.a
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
       /Users/ohoskt/.konan/dependencies/llvm-1201-macos-aarch64/bin/ld.lld
       --sysroot=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot
       -export-dynamic
       -z
       relro
       --build-id
       --eh-frame-hdr
       -dynamic-linker
       /lib/ld-musl-aarch64.so.1
       -o
       /Users/ohoskt/git/sample/kn_samples/build/bin/ohosArm64/releaseShared/libc2k.so
       /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/Scrt1.o
       /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crti.o
       /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos/crtn.o
       --hash-style=gnu
       -L/Users/ohoskt/.konan/dependencies/llvm-1201-macos-aarch64/lib/aarch64-linux-ohos
       -L/Users/ohoskt/.konan/dependencies/llvm-1201-macos-aarch64/lib/aarch64-linux-ohos/c++
       -L/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/sysroot/usr/lib/aarch64-linux-ohos
       --gc-sections
       -S
       -shared
       --soname=libc2k.so
       /private/var/folders/0v/kpcstls94gd53xpxw5frkddh0000gn/T/konan_temp15306597006144440165/libc2k.so.o
       /var/folders/0v/kpcstls94gd53xpxw5frkddh0000gn/T/included11769682759184190092/libadd.a
       --exclude-libs=libadd.a
       -Bstatic
       -Bdynamic
       -ldl
       -lm
       -lpthread
       -lc++
       -lc++abi
       --defsym
       __cxa_demangle=Konan_cxa_demangle
       -lc
       -lunwind
       -lqos
       -lhitrace_ndk.z
       -lhilog_ndk.z
    

问题场景

下面来构建一个有死代码的场景,在 add.c 中加一个 deadFun,libadd.a 中会多一个 GLOBAL DEFAULT 的符号

1
2
3
4
5
6
7
8
9
10
11
12
13
➜  kn_samples git:(static-cinterop-published) ✗ llvm-readelf -s add/src/nativeInterop/add/libadd.a 

File: add/src/nativeInterop/add/libadd.a(add.o)

Symbol table '.symtab' contains 6 entries:
   Num:    Value          Size Type    Bind   Vis       Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT   UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT   ABS add.c
     2: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT     2 $x.0
     3: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT     3 $d.1
     4: 0000000000000000    32 FUNC    GLOBAL DEFAULT     2 addCFun
     5: 0000000000000020     4 FUNC    GLOBAL DEFAULT     2 deadFun
➜  kn_samples git:(static-cinterop-published)

最终kn的so也会导出这个符号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  kn_samples git:(static-cinterop-published) ✗ llvm-readelf --dyn-syms build/bin/ohosArm64/releaseShared/libc2k.so

Symbol table '.dynsym' contains 199 entries:
   Num:    Value          Size Type    Bind   Vis       Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT   UND 
     1: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT   UND main
                ... ...
   133: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND _Unwind_RaiseException
   134: 00000000000b2a70   200 FUNC    GLOBAL DEFAULT    12 add_c_name # @CName,kotlin的so开给c调的函数
                ... ...
   174: 00000000000c200c    32 FUNC    GLOBAL DEFAULT    12 addCFun # 静态库中给kotlin调的函数
                ... ...
   183: 00000000000c202c     4 FUNC    GLOBAL DEFAULT    12 deadFun # 静态库中的死代码,
                   ... ...
➜  kn_samples git:(static-cinterop-published)

实际的场景 deadFun 可能是静态库中没有用到的一些功能,打进kn的so会增加包体积,相关的死代码和有被调用的代码进入一个page的话,会被一起交换进物理内存,增加内存占用

优化方案

优化方案的一个关键假设是这些静态库中提供给kotlin代码调用的方法全都不需要再从kn的so开放出去,给其他 c/cpp 调用。

  • kn打包出的头文件不会包含这种静态库里的接口,如果有调用大概率引用了静态库的头文件
  • 也可以通过查看hap中所有其他的so的动态符号表是否有Ndx=UND,Name在静态库中有定义的符号大概确认(通过dlopen dlsym调用不会体现在符号表中,所以这种方式也不能100%确认) 最终的收益和静态库中实际有多少死代码强相关

在链接时进行死代码删除的效果通常比编译时差。编译时在llvm ir上进行dce可以做到粒度非常细,比如函数中有一个一定走不到的else分支可以将这个分支的代码删除。链接时的 –gc-sections 选项只能删除elf中整个没用的section,最细的粒度只能到函数级进不到函数的实现中。链接时要进行有效的死代码移除需要两点:

  1. 精确的gc root
  2. 尽可能小的elf section

对一个so来说动态符号表中 GLOBAL/WEAK DEFAULT 的,开放给其他so调用的函数明显是要保留完整实现的,这些函数递归往下用到的所有其他函数和数据也都要保留。因此导出过多的符号会导致so中存在无效代码。当然也要注意正确性,如果实际用到了的符号没有导出,调用时会发生无法catch的崩溃。在cinterop静态库的场景下可以在def中添加 –exclude-libs 控制静态库中的符号不从最终的so中导出

1
2
3
4
5
package = add
headers = add.h
staticLibraries = libadd.a
libraryPaths = src/nativeInterop/add
linkerOpts = --exclude-libs=libadd.a

在 dynsym 中看不到 addCFun 和 deadFun 后,会发现他们仍在 symtab 中,deadFun 仍没被删除。这是因为在静态库中这两个符号被打包到了同一个.text section中,这样 addCFun 有用就是这个section有用,保留这个 section 就导致 deadFun 被保留。针对这种情况需要在编译静态库时添加 -ffunction-sections 和 -fdata-sections,让所有函数和一些全局/静态变量被打进独立的section,可以独立进行删除。

1
2
3
4
5
6
7
8
# Build the static library for add (used by the published cinterop klib)
cd add/src/nativeInterop/add
"${LLVM_BIN}/clang" \
    --sysroot "${SYSROOT}" \
    --target=aarch64-linux-ohos \
    -O3 -fPIC \
+    -ffunction-sections -fdata-sections \
    -c add.c -o add.o

cmake中选项通过 target_compile_options(add PRIVATE -ffunction-sections -fdata-sections) 添加

All Rights Reserved.