Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Scala Native 0.5.0がリリースされた (バイナリサイズが6割に)

先日(4月11日)、Scala Native 0.5.0がリリースされた。やったー!

scala-native.org

というわけで変更点や成果物のサイズなどを見ていこう。

変更点

網羅的な変更点はリリースノートに書いてあるので、主に利用者側に影響が大きそうなものを取り上げてみる。

マルチスレッディングのサポート

これがv0.5.0最大の目玉。これまでScala Nativeはマルチスレッドをサポートしておらず、並行処理ライブラリであるCats Effectなどはシングルスレッド動作をさせてお茶を濁すしかなかったし、OSまわりの操作をやってくれる便利ライブラリのos-libも、スレッドを生やせない都合でos.procを利用したプロセスのスポーンが未実装となるなど、利用上の制限がけっこう大きかった。v0.5.0がリリースされてマルチスレッディングがサポートされたことにより、Scala Nativeでビルドされたアプリケーションのパフォーマンスはさらに増大することが予想されるし、より一歩Scala Nativeがプロダクションレベルに近付いたことを意味している。

32ビットアーキテクチャのサポート開始

型のサイズまわりの調整が終わり、Scala Nativeは32ビット環境に向けてコンパイルできるようになった。古めのラズパイなんかに使えるようになる。地味に嬉しい変更だ。

ソースレベルデバッグのサポート

デバッグ用の設定が拡充された。macOSでデバッグするとき、おおまかなソースコード上の行数を表示できるようになった。詳しくはドキュメントを見てほしい。

デフォルトの出力先バイナリファイル名の変更

これまでは プロジェクト名-out のような名前だったが、プロジェクト名がそのまま使われるようになった。設定可能なのでドキュメントを見てほしい。

便利タスクの追加

これまではバイナリを生成するためにnativeLinkを利用していたが、リリースの利便性のためにnativeLinkReleaseFastnativeLinkReleaseFullが用意された。それぞれを利用することで、リリースモードを上書きしてビルドできる。普段はデバッグ設定でビルドしておき、リリース時に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)
    }
  )

ビルド設定は以前の記事から持ってきた、シングルバイナリを吐き出すやつ。

blog.3qe.us

差分は、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回くらいビルドしなおしちゃった。

たらい回し関数対決

マルチスレッドまわりはまだ関連ライブラリのポーティング作業中ゆえにすぐ行えないのだが、シングルスレッドの性能評価くらいだったらすぐできる、というわけでおなじみのたらい回し関数を呼び出すことにした。

ja.wikipedia.org

今回は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はさすがにキツいけど。

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?