Open5

k2 compilerはなぜ速いのか

ノアノア

記事にまとめる前段階の下書き

まえがき

Kotlin 2.0.0から安定版になったK2 compiler。使ってますか?
ビルド速度の面で明確にメリットがあるため、業務で利用したいと考えました。

「使いたい!」と言ったところ、寛容な風土というか変化に耐性があるというか、ですんなりOKが出てしまったのですが「なぜ早いのか」を調べておくことにしました。

  • 国内外のK2 compilerに対する説明の整理
  • 導入するためにはどうするか
  • 速度計測例

対象読者

すでにkotlinを使ってコーディングしている開発者。

ノアノア

前提「そもそも速いことに意味ある?」

ある。

compileの速度は全ての速度に関係する。
動作確認、テスト、検証環境へのデプロイ...全ての開発業務はbuildを避けて通れない。
それが1秒でも短いことは全ての工数を削減する。

また、今の開発中のプロダクトがbuild速度に不満がなかったとしてもそれは改善しない理由にはならない。
プロダクトは成長し続ける限りコードの肥大化は避け難い。
今、ムキムキのbuildも鍛え続けなければいずれはシナシナになる。

速さは未来を作り続けるために必要である。

ノアノア

K2 Compiler, Kotlin 2.0って?

K2 compilerはKotlin 2.0.0で安定版として導入された機能です。
これまでの機能を活かしつつ、ビルドを高速化、機能を強化したものであり2.0.0の目玉です。

JETBRAINS公式曰く…

https://blog.jetbrains.com/ja/kotlin/2024/05/celebrating-kotlin-2-0-fast-smart-and-multiplatform/ より

K2 コンパイラーが導入されたことにより、すべてのコンパイラーバックエンドが多数のロジックを共有し、統一のパイプラインを使用できるようになったため、Kotlin がサポートするすべてのプラットフォームを統一できるようになりました。 その結果、ほとんどの機能、最適化、およびバグ修正をすべてのプラットフォームにまとめて実装できるようになったため、新しい言語機能の開発速度を大幅に高められます。 この新しいアーキテクチャでは、マルチプラットフォームプロジェクトのコンパイル時間をさらに改善することも可能です。

Kotlin 2.0 では新しいコンパイラーフロントエンドのおかげでコンパイル速度が向上し、場合によっては 2 倍になる可能性があります。 コンパイル速度はプロジェクトによってもう少し速かったり若干遅くなったりする可能性もありますが、全体的には以前の Kotlin バージョンよりも実際のプロジェクトのコンパイル時間が大幅に向上することを確認しています。

では何が変わったのか

フロントエンドが一新

https://blog.jetbrains.com/wp-content/uploads/2024/05/k2-compiler-architecture.png

このNew frontendが肝。
https://speakerdeck.com/kitakkun/kotlin-fest-2024-motutokotlinwohao-kininaru-k2shi-dai-nokotlin-compiler-pluginkai-fa?slide=4
↑のKotlin Fest2024の発表が詳しい。

Tips: compilerのFrontend/Backend

ref: https://zenn.dev/tommykw/articles/69c64b13d8aec4

コンパイラにおけるフロントエンド/バックエンドはサービス開発におけるフロントエンド/バックエンドの意味とは異なります。

  • フロントエンド : ソースコード(*.kt)を中間表現に変換までを担当
  • バックエンド:中間表現からバイナリへの変換までを担当

この2つの工程でKotlinのソースコードをJVMのバイナリコードに変換するのがコンパイラの責務です。

フロントエンドの大改修、それが今回コンパイラ全体で速くなった理由の大部分です。

以前のコンパイルでは2手にわかれて解析していたので遅かった。
それが同じ中間表現から全てのコードを作れるようになったのだから速いに決まっている。
※それはそれとして出力物の種類が多いほどに効果は大きくなるというわけで、バックエンドのビルドしかやらないのであれば最大の効果は得られなさそうに見える。

ノアノア

📝 PSI

Program Structure Interface (PSI) tree

PSI(Program Structure Interface)はJetBrainsの木構造解析API
ref: https://zenn.dev/tommykw/articles/69c64b13d8aec4 (※ver. 1.5.0時点の記事)

📝 Binding Context

k2 compiler以前のフロントエンドコンパイルの生成物の一部。
後述のFIRと合わせてバックエンドコンパイルに渡していた。

Map構造だから遅いらしい

ref: https://speakerdeck.com/sansantech/sansan-20240708-3?slide=6

📝 FIR

FIR=frontend intermediate representation
フロントエンドコンパイルが生成する成果物。
k2 compilerからはこれだけをバックエンドコンパイルに渡すようになった。

ノアノア

では使おう

ここ↓をバージョン更新するだけ!

in build.gradle.kts

    kotlin("jvm") version "2.0.10"
    kotlin("plugin.spring") version "2.0.10"
    kotlin("plugin.jpa") version "2.0.10"
    kotlin("plugin.allopen") version "2.0.10"

※2024/8時点のpatch ver. upを経た最新版が2.0.10だったというだけで時期によって最新ver.は異なります。

速度計測

計測方法

KdApplicationのBuildで計測しました!

↓を記載するとbuild結果をファイルで出力できるようになるので…

$ git diff
diff --git a/gradle.properties b/gradle.properties
index 8fe20ee35b..9cdd793d88 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1 +1,2 @@
 org.gradle.jvmargs=-Xmx4096m
+kotlin.build.report.output=file
\\ No newline at end of file

Before:kotlin 1.9.23, After: kotlin2.0.10で以下を実行

./gradlew build --no-build-cache --no-daemon

結果ファイルからcompile時間を取り出して比較します。
※compileKotlinとcompileTestKotlinの合算です。

計測結果

Before

        Sources compilation round: 125.76 s
          Compiler initialization time: 1.41 s
          Compiler code analysis: 87.69 s
          Compiler code generation: 31.29 s

After

        Sources compilation round: 54.08 s
          Compiler initialization time: 0.20 s
          Compiler code analysis: 23.98 s
          Compiler IR translation: 4.44 s
          Compiler code generation: 25.27 s
            Compiler IR lowering: 8.43 s
            Compiler IR generation: 16.84 s

build自体は公式に言われている通り2倍速以上の速度が出ているのがわかります 👀

ちなみにTest実行にかかる速度は変わらないのでどちらもtestの工程で800秒以上がかかり、
合計ビルド時間は15分前後になります。。。