fthomas/refinedのScala 2 macroの更なる速度最適化

まずは以下の以前の記事を読んでおいてください

xuwei-k.hatenablog.com

さて、

前回の爆発と比較したら相対的には大したことないのですが、refinedのmacroには、まださらに改善の余地があるのでその話をします。

前回の記事で

特定の事前定義された型ならば、そこから Validate のインスタンスを取得、見つからなかったらevalしています。

と書きました。しかし、よくプロファイルしてみると、まだ改善の余地があることがわかりました。 事前定義された型ならば、そもそも Validate のinstanceは内部に保持してあるので、わざわざimplicitで受け渡す必要がありません。

このimplicitで引き渡す Validate のinstanceの生成が微妙に遅いことがわかりました。 よって

  • 以下のように Validate を受けとらず、事前定義された型以外はcompile errorにするように改造してしまう
  • 本来のmacroと、独自改造したmacroでcompile速度を比較

https://github.com/xuwei-k/fthomas-refined-macro-benchmark/commit/56268e956e0611bf9960f8bee67c6e74946c579d

https://github.com/fthomas/refined/blob/1e080f64496e5ad81d7281da5c4a26e8a49a9a13/modules/core/shared/src/main/scala-3.0-/eu/timepit/refined/macros/RefineMacro.scala

3c3
< import eu.timepit.refined.api.{RefType, Validate}
---
> import eu.timepit.refined.api.{RefType, Refined, Validate}
10c10
< class RefineMacro(val c: blackbox.Context) extends MacroUtils with LiteralMatchers {
---
> class MyRefineMacro(val c: blackbox.Context) extends MacroUtils with LiteralMatchers {
14,17c14
<   def impl[F[_, _], T: c.WeakTypeTag, P: c.WeakTypeTag](t: c.Expr[T])(
<       rt: c.Expr[RefType[F]],
<       v: c.Expr[Validate[T, P]]
<   ): c.Expr[F[T, P]] = {
---
>   def impl[T: c.WeakTypeTag, P: c.WeakTypeTag](t: c.Expr[T]): c.Tree = {
25c22,24
<     val validate = validateInstance(v)
---
>     val validate = validateInstance[T, P].getOrElse(
>       abort(s"could not found Validate[${weakTypeOf[T]}, ${weakTypeOf[P]}]")
>     )
26a26
> 
30c30
<     c.universe.reify(rt.splice.unsafeWrap[T, P](t.splice))
---
>     c.universe.reify(reify(RefType[Refined]).splice.unsafeWrap[T, P](t.splice)).tree
33,40c33
<   def implApplyRef[FTP, F[_, _], T: c.WeakTypeTag, P: c.WeakTypeTag](t: c.Expr[T])(
<       ev: c.Expr[F[T, P] =:= FTP],
<       rt: c.Expr[RefType[F]],
<       v: c.Expr[Validate[T, P]]
<   ): c.Expr[FTP] =
<     c.Expr[FTP](impl(t)(rt, v).tree)
< 
<   private def validateInstance[T, P](v: c.Expr[Validate[T, P]])(implicit
---
>   private def validateInstance[T, P](implicit
43c36
<   ): Validate[T, P] =
---
>   ): Option[Validate[T, P]] =
53d45
<       .getOrElse(eval(v))

という実験をしました。実験の詳細な条件は

手元で実験した結論としては、全体のcompile速度は

  • 本来の eu.timepit.refined.auto 経由のmacroだと23秒
  • 独自に改造したmacroだと3秒

という結果になりました。 JMHなど使って正確に計測したわけではなく、普通にsbt compileをsub project毎に順番に実行しただけなので、厳密に頑張りたい場合はもっと丁寧に実験したほうがいいですが、今回は1万個定義したおかげもあって、明らかな有意差が出ているため、これでひとまず十分でしょう。

-Yprofile-trace で実際に内部の結果を詳細に記録もしたので、それも貼って解説しておきます。

  • 両方ともtyper以外の後半のphaseの時間は、ほぼ1秒で差がない
  • つまり、typerで、それぞれ2秒と22秒で、ほぼ10から11倍の結果である
  • 具体的に1つのdef毎に見た場合(全部綺麗に平均を取ったわけではなく、適当に抽出したもの)
    • オリジナルの eu.timepit.refined.auto : 1.7から1.8ミリ秒
    • 独自改造した方: 0.16ミリ秒
    • 全体が約10から11倍、というのと辻褄があう
  • 拡大してみると、例えばshapelessの ToInt などのinstance生成や、その生成のためのshapeless内部のmacroが地味に時間を消費していることがわかる
  • macro本体を直接改造せずに、もう少しマシに?同等レベルで速く?する方法が存在する気がするけれど、あまり試してない

https://github.com/xuwei-k/fthomas-refined-macro-benchmark/commit/eddc8b13e19ee60c645e66107ddcd69037430b13

というわけで、結局Scala 2でfthomas/refinedのmacroを使う場合で、compile速度も重視したい場合、大量に使う予定がある場合は、 これらの速度劣化を覚悟して使うか、独自に改造するなどの工夫まで視野に入れて使いましょう。

また、今回は NonNegInt で試したのでshapelessのToIntが関係してきましたが、他のStringなどの型で同じような傾向の結果になるのか?は試してません。 気が向いたら試して追記するか別に書くかもしれないし、書かないかもしれません。

オリジナルの本体のautoのmacroの全体図

オリジナルの本体のautoのmacroの拡大図

オリジナルの本体のautoのmacroの一番拡大した図

独自macroの全体図

独自macroの拡大図