KMP&CMP 鸿蒙优化实战 

编译构建优化:KMP 工程编译慢?实测并行编译与模块化编译

上一篇,我们把 Kotlin 代码到 libkn.so、再到鸿蒙 HAP 的构建链路带大家走了一遍。这一篇继续解决开发时更直接的问题:代码改完了,为什么还要等这么久?

我们要缩短等待的时间,得先找到慢在哪。本文围绕这一主题介绍两条路径:让一次 Native 构建中的部分工作并行处理,以及把一个大产物拆成多个模块产物,再安排构建。它们可以组合,但开关越多、并发越高,并不代表一定越快。

01 先看清楚我们在等什么

从我们点下构建到应用出现在手机上,KMP 至少经过几段不同的工作:

  1. Gradle 配置工程和准备依赖;
  2. Kotlin 代码编译为 klib(klib 可以先理解为 Kotlin/Native 的中间库文件);
  3. Native 后端进行代码生成、优化和链接;
  4. so 与头文件拷入鸿蒙壳(so 是鸿蒙侧最终加载的动态库);
  5. 最后由鸿蒙工具链打 HAP、安装并启动。

在整条编译链路中,klib 准备好了,不代表 so 已经生成;名字里带 link的任务,也不只是把现成文件拼在一起,它还会涉及 Native 后端处理。

所以,「Gradle 很慢」只是表面现象。如果时间主要花在依赖下载,先解决仓库和网络;如果主要卡在 Native 产物生成,可以参考本文的优化手段;若卡在 HAP 打包和安装,调 Kotlin 参数也未必能明显减少等待。

shell./gradlew :composeApp:linkReleaseSharedOhosArm64 --profile

这条命令只构建单包 Release 产物,并生成本地 Gradle 性能报告。终端会提示报告位置,通常在 build/reports/profile 下。先看各任务耗时,再决定要改哪里:

02 两种并行方式

CPF Kotlin 额外提供了两种并行编译的方法供开发者按需使用(Tip:以下优化只针对 Release 编译有效)。

2.1 Native 构建内部并行

第一种是 Native 构建内部的并行,原理是将 LLVM IR 切分后多线程执行局部优化后合并,缩短整体的编译等待时间。CPF Kotlin 提供了 splitBCfile的编译参数,它的作用是单模块内多任务并行编译,可以理解为将一份 Kotlin 代码分成多份并发执行:

gradle args-Xbinary=splitBCfile=4  // 代表并行度 4

2.2 模块化构建

第二种是多个模块构建并行,先把大产物拆成独立模块,再由开发者选择将不同的模块在分布式场景中构建。它与并行编译的优化手段不同:上面的构建最后还会集中输出成一个 so 文件,而模块化构建最终会基于拆分的模块,分别输出单独的 so 文件。

03 先试一次构建内部的并行

我们在 sample 中的 composeApp/build.gradle.kts 配好并行度参数,默认值为 4:

配置了这个参数后,Kotlin Bitcode 会按拆分数启动异步处理,为分片创建独立 LLVM context,等待处理结果后继续后续流程。因此,「4」表示拆分数,不是承诺整个构建恰好使用四个线程,更不是保证快四倍。

如果耗时主要集中在这段可并行处理的工作,多核机器可能获益;但拆分、合并和额外内存也有成本。工程较小,或者内存已经紧张时,也可能收益有限甚至变慢。最终以实际对照结果为准。

在执行过程中,我们可以看到编译器调用了 llvm-split,首先进行了代码拆分。

llvm-split 代码拆分过程〕

在小工程下收益不大,但是如果是中大型工程,并行编译能提供可观的收益(约 20%),现在我们用大型工程实测一下效率提升:

指标

开启分片 4

关闭分片

Release link 耗时

29.17 分钟

35.91 分钟

整次 Gradle 执行耗时

29.41 分钟

36.18 分钟

有效测试次数

3 次

3 次

并行路径

开启

关闭

本次开启 4 份并行后,link 耗时减少了 23.09%。

04 模块化编译在这里具体拆什么

模块化特指 Native 动态库产物拆分,不是先把业务目录重构成一堆 Gradle module。sample 会读取 klib 依赖分析形成的映射,为不同模块注册 sharedLib binary 和 link 任务,典型场景如下:

pipeline单包模式:  Kotlin 与依赖  → libkn.so模块化模式:运行时       → libruntime.so标准库       → libstd.so依赖模块     → libmodule_*.so应用入口模块 → libkn.so

4.1 如何拆分模块

我们可以使用编译选项 printModule来输出所有的模块。以我们的工程为例,在 Task 中临时加入 printModule=true,执行一次编译会在命令行打印出所有可拆分的模块:

〔printModule 输出的可拆分模块列表〕

4.2 配置系统模块

我们还需要额外配置 std 和 runtime 两个 Kotlin 系统模块,你可以理解为这是模块化预置的两个基础系统模块,必须要分开拆分配置,不能与业务模块合并:

几个关键参数可以这样理解:

configemitRuntime=true   输出运行时emitStdlib=true    输出标准库moduleIncludes     描述各模块包含哪些 kliboutputModule      指定当前输出哪个模块

4.3 配置业务模块

下一步是基于上一步列出的业务模块拆分(去掉 std 和 org.jetbrains.kotlin.native.platform),来划分 so。经过筛选我们遗留了以下模块,其中一些是我们的业务代码,还有一些是依赖的三方库:

module listkmptcp_kotlin_sample:lib-basekmptcp_kotlin_sample:lib-arithkmptcp_kotlin_sample:lib-analyticskmptcp_kotlin_sample:lib-networkkmptcp_kotlin_sample:lib-storageorg.jetbrains.kotlinx:atomicfu-cinterop-interoporg.jetbrains.kotlinx:atomicfuorg.jetbrains.kotlinx:kotlinx-coroutines-core-cinterop-interoporg.jetbrains.kotlinx:kotlinx-coroutines-corekmptcp_kotlin_sample:composeApp-cinterop-samplekmptcp_kotlin_sample:composeApp

我们把业务代码和三方库分别拆成两个 so。

以 module 模块为例,moduleIncludes配置为:

gradle argsfreeCompilerArgs += "-Xbinary=moduleIncludes={module:[kmptcp_kotlin_sample:lib-base;kmptcp_kotlin_sample:lib-arith;kmptcp_kotlin_sample:lib-analytics;kmptcp_kotlin_sample:lib-network;kmptcp_kotlin_sample:lib-storage;kmptcp_kotlin_sample:composeApp-cinterop-sample;kmptcp_kotlin_sample:composeApp]}"

outputModule必须与模块映射中的 key 一致。

4.4 模块化构建

依次执行以下编译命令:

shell./gradlew :composeApp:linkStdReleaseSharedOhosArm64./gradlew :composeApp:linkRuntimeReleaseSharedOhosArm64./gradlew :composeApp:linkThirdpartyReleaseSharedOhosArm64./gradlew :composeApp:linkModuleReleaseSharedOhosArm64

我们就会得到模块化之后的产出物:

〔模块化构建的产出物〕

最终我们把这些 so 文件都放到鸿蒙壳工程里面去就可以了。如果这些命令能放在不同的机器上执行,就能显著提高工程的编译速度!

05 结合一起使用

我们也可以把模块化和并行化结合在一起使用,它们之间互不冲突。比如在 module 模块的编译中我们也希望开启并行化,只要在对应的配置中加入并行化的编译参数就可以了。

06 常见问题

· 为什么并行编译设置为四份了还是看不出并行?先检查目标是不是 ohosArm64、是否使用 Release、是否仍设置了 debuggable=true,以及参数是否加到了正在执行的 binary 上。给一个 sharedLib 配置设置并行参数,不会自动影响到其他 sharedLib,需要开发者单独配置。

· 为什么开四份没有更快甚至更慢?并行编译只覆盖部分阶段。前序分析占比高、工程较小、内存紧张或拆分开销较大时,都可能抵消收益。先看实际耗时和内存状态,不要只看 CPU 曲线,也不要把「4」理解为四倍提速。

· 为什么模块化配置后报语法错误或找不到模块?先检查字符串是否闭合,outputModule 是否与映射 key 一致,moduleIncludes 中的 klib 唯一名称是否正确,以及不同模块是否使用完整一致的映射。仅复制构建命令,不能替代这些配置检查。

· 为什么生成了多个 so,鸿蒙应用却启动失败?检查是否漏拷 runtime、std 或依赖模块,是否混用了不同工具链或构建批次的头文件与 so,以及壳层链接、注册与加载配置是否已适配拆分。ABI也就是二进制之间的调用约定,需要整套依赖保持兼容。构建成功后仍要做启动与功能验证。

07 小结

Native 内部并行优化的是一次构建中的部分处理过程,最终仍输出一个 so;模块化构建调整的是产物边界,最终输出一组配套 so。两种方式可以组合,但收益需要分别验证。

建议是:先用一个基本工程跑通参数和产物,再用真实工程比较耗时;先验证单一变量,再考虑组合优化。判断优化是否值得,既要看等待时间,也要看内存成本、产物管理以及最终运行结果。

即刻体验

CPF-KMP-CMP 鸿蒙平台首个正式版本发布说明

CPF(Cloud Power Foundation)KMP 与 CMP 已发布首个正式版本,为鸿蒙平台带来完整的 Kotlin Multiplatform 开发支持。构建链路、并行编译与模块化构建,都在这个版本里等你验证。

快速开始

https://atomgit.com/CPF-KMP-CMP/docs

发布公告

https://atomgit.com/org/CPF-KMP-CMP/discussions/15

欢迎参与社区共建,一起把 KMP 鸿蒙开发体验做得更快、更顺。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐