Fundsのコンパイル速度を改善した話

6d87223b5fcfb9a5a73007f77f134e28?s=47 taketora
September 15, 2020

 Fundsのコンパイル速度を改善した話

【オンライン開催】09/15(火) Scala関西勉強会 - 2020年09月版

https://connpass.com/event/184638/

サービスが成長するとコードが増えて、コンパイル速度に時間がかかることもあるかと思いますが、FundsのサーバーサイドのScalaプロジェクトのコンパイル速度を44%短縮しました。
コンパイル速度が遅くなる要因や、改善していったプロセスなど発表します。

6d87223b5fcfb9a5a73007f77f134e28?s=128

taketora

September 15, 2020
Tweet

Transcript

  1. Fundsのコンパイル速度を改善した話 2020/09/15 Scala関西勉強会 @taket0ra1

  2.  Caution Fundsの商品の勧誘を行うものではありません 汎用的にcompile速度を改善するチップス でもありません。 Scala2系においてコンパイル速度のボトルネックを見つけ て解決した内容になります

  3. 自己紹介

  4. @taket0ra1 Septeni Original 2015/07 サーバーサイドエンジニア 2018/04 教育担当、採用担当 2020/08 退職 Stanby 2020/09 サーバーサイドエンジニア Tech Academy

    2018/03 Scala講師(業務委託) Funds 2019/04 サーバーサイド(業務委託)
  5. Fundsについて

  6. None
  7. 資産運用スタートアップの開発で採用した PlayによるClean Architectureでの 設計・開発事例 株式会社クラウドポート 若松 慶信 @Scala 関西 Summit

    2019 資産運用スタートアップの開発で採用した、 PlayによるClean Arcitectureでの設計・開発事例 より引用
  8. 今回話す内容 Fundsのバックエンド(Scala)のコンパイル速度を改 善していったプロセスを紹介していきます

  9. 質問

  10. 普段関わっているScalaのプロジェクトの コンパイル速度はどのくらいかかっていますか? A. 30sec 以下 B. 30sec ~ 60sec C.

    60sec ~ 120sec D. 120sec ~ 180sec E. 180sec 以上
  11. 2019年11月時点のバックエンド(Funds)のコンパイル速度 [info] Compiling 1348 Scala sources and 4 Java sources

    to  target/scala-2.12/classes ... [info] Done compiling. [success] Total time: 386 s, completed 2019/11/27 20:47:16 6分以上時間がかかっていた 前提条件 • sbtにはメモリ4GBを割り当て • sbtを起動しclean、compileを行った2度目の結果 • 依存性のライブラリはダウンロード済みの状態 環境 • Scala 2.12 • Java 1.8
  12. ちなみに2020年6月時点 検証する前にコンパイル速度が大幅に改善していた $ compile [info] Compiling 1353 Scala sources and

    4 Java sources to target/scala-2.12/classes ... [info] Done compiling. [success] Total time: 133 s, completed 2020/06/02 6:31:33 386 secから133 sec に改善されていた。
  13. 考えられる要因 • Scala のマイナーバージョンアップ ◦ 2.12.4 → 2.12.10 ◦ Scala2.12.9はScala2.12.8よりも5-10%速い ▪

    なおScala2.13.0は既存の5-10%速い
  14. 考えられる要因 • Javaのバージョンアップ ◦ Java1.8 → Java11 • Play Framework

    ◦ 2.6.9 → 2.6.25 • 不要になった機能のソースコードの削除 コンパイルの改善にバージョンアップが重要
  15. 今回検証は 133secからさらにコンパイル速度を早くする施策

  16. ScalaのCompilerとは

  17. 役割 Scalaで記述されたソースコードを解析して JVMバイトコードを生成する

  18. https://github.com/scala/scala/tree/2.13.x/src/compiler Scalaで実装されている大規模なコード

  19. Scala2.12.10 では 24フェーズ ※Scala 2.13.3も同様

  20. ちなみにDotty のフェーズ (0.27.0-RC1)

  21. 検証

  22. 1. マルチプロジェクト化

  23. $ compile [info] Compiling 1353 Scala sources and 4 Java

    sources to target/scala-2.12/classes ... [info] Done compiling. [success] Total time: 133 s, completed 2020/06/02 6:31:33 元々シングルプロジェクトだったので、どのレイヤーでどれだけ時間が かかっているか把握できなかった。 1. マルチプロジェクト化
  24. Fundsの前提知識の共有

  25. システムアーキテクチャ Backend Service (Scala) DB, Datastore, External REST Services, etc.

    Service frontend Admin frontend REST API REST API 共通バックエンド サービス ドメインが複雑・密接に関係するバックエンドは モノリシックなサービスで構成 資産運用スタートアップの開発で採用した、 PlayによるClean Arcitectureでの設計・開発事例より引用
  26. Clean Architectureで定義されるレイヤー レイヤー レイヤーの概要 Entity (Enterprise Business Rules) 企業全体のビジネスルール Use

    Case (Application Business Rules) アプリケーション固有のルール・ロジック Interface Adapters 入出力・永続化・DB非依存のSQLなど Frameworks & Drivers フレームワークやドライバなど 資産運用スタートアップの開発で採用した、 PlayによるClean Arcitectureでの設計・開発事例より引用
  27. Fundsで採用しているレイヤー構造 レイヤー レイヤの用途 Domain ドメイン固有の概念・ビジネスロジック Application アプリケーション固有のロジック Interface 入出力やその表現 Infrastructure

    副作用を持つ実装 (DB, HTTP client, 外部サービス, etc.) 資産運用スタートアップの開発で採用した、 PlayによるClean Arcitectureでの設計・開発事例より引用
  28. 前提知識 • モノリシックなシステムでシングルプロジェクト構成 • クリーンアーキテクチャを取り入れている シングルプロジェクトだがレイヤーが分けられているので マルチプロジェクト化が容易

  29. 1. マルチプロジェクト化 ※簡略化しています build.sbt

  30. > compile [info] Compiling 13 Scala sources to library/target/scala-2.12/classes ...

    [info] Done compiling. [info] Compiling 380 Scala sources to domain/target/scala-2.12/classes ... [info] Done compiling. [info] Compiling 288 Scala sources to application/target/scala-2.12/classes ... [info] Done compiling. [info] Compiling 317 Scala sources and 4 Java sources to interfaces/target/scala-2.12/classes ... [info] Done compiling. [info] Compiling 346 Scala sources to infrastructure/target/scala-2.12/classes ... [info] Done compiling. [info] Compiling 9 Scala sources to funds/target/scala-2.12/classes ... [info] Done compiling. [success] Total time: 136 s, completed 1. マルチプロジェクト化
  31. レイヤー compile time(sec) library 3.17 domain 11.2 application 12.74 interfaces

    14.49 infrastructure 85.72 funds 8.61 total 136 • コンパイル速度は変化なし • infrastructureで時間がかかっている ことが可視化 • 開発時に局所的にcompile可能 • build時間が短縮された (jarファイルを生成する時間) ◦ 735sec → 489sec 1. マルチプロジェクト化
  32. infrastructureのどこで時間がかかっているのか?

  33. SPEEDING UP COMPILATION TIME WITH SCALAC-PROFILINGより引用 The report suggests that

    about 84.3% of the compilation time is spent on typer. This is an unusual high value. Typechecking a normal project is expected to take around 50-70% of the whole compilation time.
  34. Scala2.12.10 では 24フェーズ ※Scala 2.13.3も同様

  35. the meat and potatoes: type the trees

  36. 肉とじゃがいも 

  37. ※私の解釈 typerはScalaの型システムを処理するための 主要部分という意味 Weblio 辞書より引用

  38. typerフェーズでやっていること • 型を推論 • 型が一致するか確認 • 暗黙のパラメータ(implicit parameter)を探索し、構文木に追加 • 暗黙の変換(implicit

    conversion)を行う • オーバーロードを解決 • 親の参照型をチェック • implicit探索 • マクロ展開 • ケースクラスの追加メソッドを作成(copy,applyなど) Scala Compiler Phases with Picturesより
  39. 過去のコンパイラにまつわる発表でもtyperフェーズの処理で 時間がかかっているという内容が多かった Scalaのコンパイル速度の話が聞きたいだろうし、するつもりだ • 遅くなる要因 ◦ implicitなどの型推論 ◦ 無名クラスの大量生成 ◦

    マクロ Scalaのコンパイルを3倍速くした話 • 遅くなる要因 ◦ マクロ ◦ context.eval()
  40. Fundsの場合 Typerフェーズで時間がかかりそうな箇所

  41. 暗黙のパラメータ

  42. 資産運用スタートアップの開発で採用した、 PlayによるClean Arcitectureでの設計・開発事例より引用 DIのためにimplicitで宣言された変数が約500以上ある。

  43. 暗黙の変換

  44. Slickでテーブルのカラムに対して独自で定義した型を適応させる場合に、 MappedColumnTypeを使ってColumnTypeを実現している。 slick-doc-ja 3.0 — ユーザ定義機能より引用

  45. MappedColumnTypeを使った例 ColumnMappersファイルに ColumnTypeの実装をまとめている • 255のimplicit conversionを定義 • 130のテーブル用のtraitにミックスイン

  46. Infrastructureの実装例: Slick trait CustomerTable extends ColumnMappers { this: Profile =>

    import profile.api._ class Customers(tag: Tag) extends Table[CustomerRow](tag, "customer") { def id = column[ID[Customer]]("id", O.SqlType("INT"), O.PrimaryKey, O.AutoInc) def email = column[String]("email", O.SqlType("VARCHAR(256)")) def status = column[CustomerStatus]("status", O.SqlType("INT")) ... type TableRecord = (Option[ID[Customer]], String, CustomerStatus) def * = (id.?, email, status) <> ( (t: TableRecord) => { val (id, email, status_) = t CustomerRow(id, email, status) }, (customer: CustomerRow) => { Option((customer.id, customer.email, customer.status)) } ) } } 資産運用スタートアップの開発で採用した、 PlayによるClean Arcitectureでの設計・開発事例より引用
  47. 時間がかかりそうな箇所 • DIに暗黙のパラメータを使っている • Slickで暗黙の変換を定義し、ミックスインしている マクロを活用したライブラリーは使っていない

  48. 検証した施策 1. マルチプロジェクト化 2. 暗黙のパラメータによるDIからMacWireによるDIへ変更 ◦ 暗黙のパラメータを削減 3. Implicit Conversion

    の定義ファイルを分割 ◦ 不要な暗黙の変換の読み込み数を減らす。
  49. 2. MacWireによるDIへ変更

  50. 暗黙のパラメータによるDI

  51. MacWireによるDI

  52. 回数 既存 MacWire 1 162 169 2 160 140 3

    137 138 4 127 151 5 130 132 6 131 136 7 139 134 8 124 140 9 131 138 10 132 133 average 137.3 141.1 • 約500の暗黙のパラメータを削減 • コンパイル速度は変化なし 2. MacWireによるDIへ変更
  53. 3. Implicit Conversion の定義ファイルを分割

  54. 255のimplicit conversionが定義されたColumnMappersをそれぞれグルーピングして分割

  55. https://github.com/slick/slick/blob/master/slick/src/main/scala/slick/jdbc/JdbcTypesComponent.scala#L484-L509 SlickのLibraryでもよく使われる型の columnTypesが定義されており 23のimplicitメソッドが定義されている 分かりやすさの観点で一つのファイルにつ き20まで定義するように変更した どのくらいの数が許容できるのか?

  56. 1つのファイルを70のファイルに分割し、各テーブルのtraitには 極力使わない暗黙の変換がスコープに入らないように対応した。

  57. 3. Implicit Conversion の定義ファイルを分割 回数 既存 ファイル分割 1 162 80

    2 160 75 3 137 75 4 127 84 5 130 80 average 143.2 (137.3) 78.8 コンパイルが遅くなっている原因は必要のない暗黙の変換を130ものtraitに ミックスインしていることだった
  58. まとめ

  59. 回数 既存 マルチ プロジェクト MacWire 分割 1 162 145 155

    80 2 160 121 133 75 3 137 119 140 75 4 127 113 151 84 5 130 120 132 80 average (sec) 143.2 (137.3) 123.6 142.2 78.8 今回の検証の結果
  60. コンパイル速度を早くするためにやった方が良いこと • 継続的にScalaのバージョンアップデートする • 不要な機能のソースコードの削除を行った方が良い またプロダクトコードが増えてコンパイル時間の増加が指数的な場合は見直した方が 良い 文献からScalaのコンパイルで時間がかかるのはTyperらしい

  61. typerフェーズでやっていること • 型を推論 • 型が一致するか確認 • 暗黙のパラメータ(implicit parameter)を探索し、構文木に追加 • 暗黙の変換(implicit

    conversion)を行う • オーバーロードを解決 • 親の参照型をチェック • implicit探索 • マクロ展開 • ケースクラスの追加メソッドを作成(copy,applyなど) Scala Compiler Phases with Picturesより
  62. 検証の結果より • 暗黙のパラメータはコンパイル速度にあまり影響がなかった。 • 一つのファイルに約250のimplicit conversionを定義して、 約130のトレイトに ミックスインしていた箇所でcompile 時間が増大していた。 •

    マルチプロジェクトにするとビルドの速度が向上した。 Fundsのケースでは、implicit conversionはまとめて定義するのではなく分割して 定義し、不要なimplicit conversionの読み込みを減らすことで コンパイル速度の向上した。
  63. ご清聴ありがとうございました

  64. もう一案 Compile時にGraalを使う 参考:JDK11でGraalを有効にするとScalaのコンパイルが13%くらい速くなった

  65. 4. Compile時にGraalを使う build.sbt • GraalはGraalVMのにおけるJITコンパイルラ • GraalはJava10以降のOpenJDKからも利用可能

  66. 4. Compile時にGraalを使う 回数 既存 GraalVM 1 162 198 2 160

    181 3 137 190 4 127 179 5 130 185 6 131 171 7 139 169 8 124 165 9 131 164 10 132 169 average 137.3 177.1 遅くなってしまったので不採用 早くなる事例があるので、実行する環境に依存 するのではないかと推測しています 検証した環境 • Scala 2.12.10 • Java 11.0.7
  67. ご清聴ありがとうございました