6.3.10 自定义 LLVM 后端的技巧

原文链接: https://kotlinlang.org/docs/native-llvm-passes.html

6.3.10 自定义 LLVM 后端的技巧

高级

Kotlin/Native 编译器使用 LLVM 为不同目标平台优化并生成二进制可执行文件。编译时间中相当可观的一部分也花在 LLVM 上,对于大型应用,这可能最终耗时长得无法接受。

你可以自定义 Kotlin/Native 使用 LLVM 的方式,并调整优化 pass 的列表。

查看构建日志

我们来看一下构建日志,以了解有多少编译时间花在 LLVM 优化 pass 上:

  1. 使用 -Pkotlin.internal.compiler.arguments.log.level=warning 选项运行 linkRelease* Gradle 任务,让 Gradle 输出 LLVM 性能分析细节,例如:
1
   ./gradlew linkReleaseExecutableMacosArm64 -Pkotlin.internal.compiler.arguments.log.level=warning

执行期间,该任务会打印必要的编译器参数,例如:

   > Task :linkReleaseExecutableMacosArm64
   Run in-process tool "konanc"
   Entry point method = org.jetbrains.kotlin.cli.utilities.MainKt.daemonMain
   Classpath = [
           /Users/user/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.0/konan/lib/kotlin-native-compiler-embeddable.jar
           /Users/user/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.0/konan/lib/trove4j.jar
   ]
   Arguments = [
           -Xinclude=...
           -library
           /Users/user/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.0/klib/common/stdlib
           -no-endorsed-libs
           -nostdlib
           ...
   ]
  1. 用这些参数再加上 -Xprofile-phases 参数运行命令行编译器,例如:
1
2
3
4
5
   /Users/user/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.0/bin/kotlinc-native \
   -Xinclude=... \
   -library /Users/user/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.0/klib/common/stdlib \
   ... \
   -Xprofile-phases
  1. 查看构建日志中生成的输出。日志可能包含数万行;带 LLVM 性能分析的部分在末尾。

下面是一个简单 Kotlin/Native 程序这样运行的摘录:

Frontend: 275 msec
PsiToIr: 1186 msec
...
... 30k lines
...
LinkBitcodeDependencies: 476 msec
StackProtectorPhase: 0 msec
MandatoryBitcodeLLVMPostprocessingPhase: 2 msec
===-------------------------------------------------------------------------===
                          Pass execution timing report
===-------------------------------------------------------------------------===
  Total Execution Time: 6.7726 seconds (6.7192 wall clock)

   ---User Time---   --System Time--   --User+System--   ---Wall Time---  --- Name ---
   0.9778 ( 22.4%)   0.5043 ( 21.0%)   1.4821 ( 21.9%)   1.4628 ( 21.8%)  InstCombinePass
   0.3827 (  8.8%)   0.2497 ( 10.4%)   0.6323 (  9.3%)   0.6283 (  9.4%)  InlinerPass
   0.2815 (  6.4%)   0.1792 (  7.5%)   0.4608 (  6.8%)   0.4555 (  6.8%)  SimplifyCFGPass
...
   0.6444 (100.0%)   0.5474 (100.0%)   1.1917 (100.0%)   1.1870 (100.0%)  Total

ModuleBitcodeOptimization: 8118 msec
...
LTOBitcodeOptimization: 1399 msec
...

Kotlin/Native 编译器会运行两个独立的 LLVM 优化序列:模块 pass 和链接时 pass。对于典型的编译,这两条流水线是接连运行的,唯一真正的区别在于它们运行哪些 LLVM 优化 pass。

在上面的日志中,两个 LLVM 优化是 ModuleBitcodeOptimization 和 LTOBitcodeOptimization。那些格式化的表格就是优化的输出,带有每个 pass 的耗时。

自定义 LLVM 优化 pass

如果上面某个 pass 看起来耗时长得不合理,你可以跳过它。不过这可能损害运行时性能,因此之后应当检查基准测试的性能变化。

目前还没有直接的方式禁用某个给定的 pass。不过你可以通过以下编译器选项提供要运行的新 pass 列表:

| 选项 | 发布版二进制文件的默认值 |

| -Xllvm-module-passes | "default<O3>" | | -Xllvm-lto-passes | "internalize,globaldce,lto<O3>" |

这些默认值会展开成一长串实际 pass,你需要从中排除不想要的那些。

要获得实际 pass 的列表,请运行 opt 工具,它会随 LLVM 发行版自动下载到 ~/.konan/dependencies/llvm-{VERSION}-{ARCH}-{OS}-dev-{BUILD}/bin 目录中。

例如,要获得链接时 pass 的列表,请运行:

1
opt -print-pipeline-passes -passes="internalize,globaldce,lto<O3>" < /dev/null

这会输出一条警告和一长串 pass,具体取决于 LLVM 版本。

opt 工具给出的 pass 列表与 Kotlin/Native 编译器实际运行的 pass 之间有两处差异:

  • 由于 opt 是调试工具,它会包含一个或多个 verify pass,而这些通常不会运行。
  • Kotlin/Native 禁用了 devirt pass,因为 Kotlin 编译器自己已经做了这些。

禁用任何 pass 之后,一定要重新运行性能测试,检查运行时性能的下降是否可以接受。