先日(4月11日)、Scala Native 0.5.0がリリースされた。やったー!
Scala Native 0.5.0 has been released!
— Scala Native (@scala_native) 2024年4月11日
The long await multithreading, initial 32-bit architectures support and many more are now available.
See https://t.co/ynLcSIJ5jK for more details
というわけで変更点や成果物のサイズなどを見ていこう。
変更点
網羅的な変更点はリリースノートに書いてあるので、主に利用者側に影響が大きそうなものを取り上げてみる。
マルチスレッディングのサポート
これがv0.5.0最大の目玉。これまでScala Nativeはマルチスレッドをサポートしておらず、並行処理ライブラリであるCats Effectなどはシングルスレッド動作をさせてお茶を濁すしかなかったし、OSまわりの操作をやってくれる便利ライブラリのos-lib
も、スレッドを生やせない都合でos.proc
を利用したプロセスのスポーンが未実装となるなど、利用上の制限がけっこう大きかった。v0.5.0がリリースされてマルチスレッディングがサポートされたことにより、Scala Nativeでビルドされたアプリケーションのパフォーマンスはさらに増大することが予想されるし、より一歩Scala Nativeがプロダクションレベルに近付いたことを意味している。
It took a while, but it's finally here! Now all we need is to update the ecosystem cross compiled for 0.5.x.
— Wojciech Mazur (@WojciechM_dev) 2024年4月11日
Hopefully it would be the last time before Scala Native 1.0 https://t.co/KklrvhWL46
32ビットアーキテクチャのサポート開始
型のサイズまわりの調整が終わり、Scala Nativeは32ビット環境に向けてコンパイルできるようになった。古めのラズパイなんかに使えるようになる。地味に嬉しい変更だ。
ソースレベルデバッグのサポート
デバッグ用の設定が拡充された。macOSでデバッグするとき、おおまかなソースコード上の行数を表示できるようになった。詳しくはドキュメントを見てほしい。
デフォルトの出力先バイナリファイル名の変更
これまでは プロジェクト名-out
のような名前だったが、プロジェクト名がそのまま使われるようになった。設定可能なのでドキュメントを見てほしい。
便利タスクの追加
これまではバイナリを生成するためにnativeLink
を利用していたが、リリースの利便性のためにnativeLinkReleaseFast
とnativeLinkReleaseFull
が用意された。それぞれを利用することで、リリースモードを上書きしてビルドできる。普段はデバッグ設定でビルドしておき、リリース時にnativeLinkReleaseFull
するといった使い方が可能だ。
アーティファクト比較
0.4系と0.5系で出力されるバイナリにどのような違いがあるか、サイズの面で見ていくことにしよう。
単にHello, World!するだけのコードを用意した。
@main def hello(): Unit = println("Hello world!") println(msg) def msg = "I was compiled by Scala 3. :)"
いつものやつという感じですね。
これを以下の設定でビルドする:
// build.sbt val scala3Version = "3.4.1" import scala.scalanative.build._ enablePlugins(ScalaNativePlugin) lazy val root = project .in(file(".")) .settings( name := "thin-scala-container", version := "0.1.0-SNAPSHOT", scalaVersion := scala3Version, nativeConfig ~= { c => c.withLinkingOptions(Seq("-static")) .withOptimize(true) .withLTO(LTO.thin) .withMode(Mode.releaseFull) } )
ビルド設定は以前の記事から持ってきた、シングルバイナリを吐き出すやつ。
差分は、Scala Nativeのバージョンだけだ。
// project/plugins.sbt addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.0")
すると以下のような結果になった。
- バイナリサイズ
- 0.4.17: 8428 KiB
- 0.5.0: 4988 KiB
- 実行時間
- 0.4.17: 1.9 ms
- 0.5.0: 1.7 ms
実行時間はほぼ一瞬なのでぶっちゃけ大した違いはないが、バイナリサイズがおそろしく小さくなっている。だいたい60%くらいになっている。ちなみにv0.5.0で作ったバイナリをstrip
したところ、1364 KiBになった。1.3 MiBはすごいぞ。しかもこれ静的リンクされたシングルバイナリですからね。Goと勝負になる領域に普通に来てますよ*1。実験してるときビックリして3回くらいビルドしなおしちゃった。
たらい回し関数対決
マルチスレッドまわりはまだ関連ライブラリのポーティング作業中ゆえにすぐ行えないのだが、シングルスレッドの性能評価くらいだったらすぐできる、というわけでおなじみのたらい回し関数を呼び出すことにした。
今回はtak(20, 10, 5)
を実行する。
@main def hello(): Unit = println("Hello world!") println(msg) println(tak(20, 10, 5)) def msg = "I was compiled by Scala 3. :)" def tak(x: Int, y: Int, z: Int): Int = (x, y) match case (x, y) if x <= y => y case _ => tak(tak(x - 1, y, z), tak(y - 1, z, x), tak(z - 1, x, y))
すると以下のような結果になった。
- 実行時間(10回測定)
- 0.4.17: 2.804 s ± 0.249 s
- 0.5.0: 3.312 s ± 0.033 s
- メモリ消費(timeコマンドで1回測定)
- 0.4.17: 8008 KB
- 0.5.0: 7240 KB
実行時間はちょっと延びてしまった。マルチスレッド機能が生えたぶんのオーバーヘッドなのかはわからないがわずかに時間がかかるようになっている。他方でメモリ使用量はわずかに減っている。いずれにせよ、ちょっとしたバイナリに要求されるものとして遜色ないパフォーマンスを出している(この計算では、2,927,486,081回takが呼び出される)。
注意点
Scala Native 0.5.0は従来のバージョンとは互換性がないため、Scala Native 0.4系のためにコンパイルされたライブラリはパブリッシュしなおす必要がある。
また、build.sbt
におけるnativeLTO :=
のようなオプションの設定記法は消え、nativeConfig ~= { c => c.withLTO(LTO.thin)
のような書き方をするようになったようだ。
まとめ
- Scala Native 0.5.0は、さらに小さい(シングル)バイナリを生成できるようになり、各種の強力な機能追加が行なわれた。
- Scala Nativeに対応させるためのポーティングが各Scala Native対応ライブラリで進行中である。
- 今後小さなfixによってパッチバージョンが上がることが予想されるため、1ヶ月ほどライブラリの対応などを待てば、ちょっとしたシングルバイナリツールの実装のために使えるようになるだろう。
- CLIツール実装のためにScalaを使うという選択肢がかなり現実的になってきた。
*1:Rustはさすがにキツいけど。