Imp と implicitly
かなり遅刻してしまいましたが、Adventar 版 Scala アドベントカレンダーの2日目です。
前日: Typelevel.scala Projects Stickers が欲しい
翌日: so_zaneli さんの finagle-toggleでデプロイとリリースを分離する
今回は implicitly と Imp ってライブラリ話を書こうと思います。
implicitly is slow
implicitly はご存知ですね。Scala 標準ライブラリの Predef
に以下のように定義されています。※ 2.11.8 時点。
@inline def implicitly[T](implicit e: T) = e
要するに型パラメータを明示的に指定してスコープ内に定義されている指定した型の暗黙的な値を取得する為のメソッドなわけですが、 こいつは大抵以下のように使われて(理屈上では?) implicitly の処理分遅いよねって話があります。
def fast[A](implicit ev: Monoid[A]): A = ev.empty def slower[A: Monoid]: A = implicitly[Monoid[A]].empty def alsoSlower[A: Monoid]: A = Monoid[A].empty // without imp
※ cats, spire の作者 Erik Osheim さんの Scala World 2015 での発表資料の84枚目より。
alsoSlower 内の Monoid#apply も資料内に定義は書いてありませんが恐らく
def apply[A](implicit A: Monoid[A]): Monoid[A] = A
あたりで implicitly と同様な実装になってることでしょう。 これも結果的には alsoSlower 内で implicitly と同様の apply 処理分遅くなります。 そして、こういった無駄な処理を削りたい!って思った時に同氏作の Imp ってライブラリを使うと良いよって話です。
Imp とは
どんなライブラリかと言うと README に書いてある下記一行で全てです。
It provides a zero-cost macro to summon implicit values.
実装もとてもシンプルでコメントや import を無視すると実装自体は実質二行。
def imp[Ev](implicit ev: Ev): Ev = macro summon[Ev] def summon[Ev](c: Context)(ev: c.Expr[Ev]): c.Expr[Ev] = ev
要するに macro を使って compile 時に暗黙的な値をそのまま置き換えて(召喚して)しまうというわけです。 使い方も README に書いてある通りとても簡単。
念のため typer phase で確認してみましょう。 適当に以下のようなコードを書いて、Show#apply の実装を書き換えて compile してみます。
class Foo() trait Show[A] { def show: String } object Show { def apply[A: Show]: Show[A] = implicitly[Show[A]] // or def apply[A: Show]: Show[A] = imp.imp[Show[A]] implicit val showFoo: Show[Foo] = new Show[Foo] { def show = "foooooooooooooo" } }
- implicitly
object Show extends scala.AnyRef { // ... def apply[A](implicit evidence$1: Show[A]): Show[A] = scala.this.Predef.implicitly[Show[A]](evidence$1);
- imp
object Show extends scala.AnyRef { // ... def apply[A](implicit evidence$1: Show[A]): Show[A] = evidence$1;
implicitly と用いた場合は apply の引数が implicitly メソッドに渡されてますが、imp を用いた場合は apply の引数がそのまま返されています。 これで implicitly の呼び出し分早くなるよ、やったね!!
implicitly も実質ゼロコスト?
喜びも束の間。近年の Hotspot の最適化の前には Imp 不要では?という話が。 README の Known Issues ってところに書いてある一文を引用します。
Dmitry Petrashko has argued persuasively that modern Hotspot optimizations mean that Imp is unnecessary. See the imp-bench repository for more information on his benchmarks.
Dmitry Petrashko*1 さんの jmh を使った benchmark
によると、Hotspot の最適化でもって implicitly も単純な値の呼び出しに置き換えられるという話が書かれているっぽいです。
せっかくなので自分の環境でも試してみました。 実行環境は以下の通り。
で、実行結果が以下の通り。baseline
が implicitly
で、measure
が imp
の結果です。
[info] Benchmark Mode Cnt Score Error Units [info] addable.ImplVsImplicitlyAddable.baseline avgt 30 2.444 ± 0.070 ns/op [info] addable.ImplVsImplicitlyAddable.explicit avgt 30 2.649 ± 0.215 ns/op [info] addable.ImplVsImplicitlyAddable.imply avgt 30 2.554 ± 0.135 ns/op [info] bench.ImplVsImplicitly.baseline avgt 30 3.025 ± 0.050 ns/op [info] bench.ImplVsImplicitly.measure avgt 30 2.905 ± 0.020 ns/op
うぅm、README の Result とは微妙に差のある結果ですね。 Error 率も差があまりないようですし、間違ってはなさそうな気もしますが...。 この最適化の方の話については別な日に詳しく追うことにします... *2
まとめ
Hotspot の最適化も優秀っぽいけど、最適化に身を委ねたくない人は Imp を使うと良いんじゃないでしょうか?