この人またcompile速度の話してる・・・
と思われるかもしれませんが、他のコストがかからずに少しでも速くなるなら速いに越したことはないし、この件、昔からのイメージで語っているのを稀に見かけますが、 測定方法がどの程度正確なのか?はともかく、とりあえず最低限は測定してから議論した方がいいよなぁ、と思ったので、計測してみた報告です。
catsというのはScalaで関数型プログラミングするための、これ書いてる時点の割とデファクトなライブラリです。
遥か昔はScalazや、その他色々なライブラリが
- 個別にimportするか
- 全部を一気にimportするか
を両方提供し、ユーザーに選択させる、という手法が取られることがあります。
そこの詳細自体はあまり解説しませんが、2025年3月現在、全部をimportする場合は import cats.syntax.all._ です。
個別にimportする場合は、例えば import cats.syntax.monad._ や import cats.syntax.traverse._ といった感じですね。
タイトルにあるようにallと個別のimportで、おそらくallの方が色々なimplicit defが全部スコープに入るため、compileが遅くなるのでは? という疑惑があります。
また、compile速度とは全く別の話で
- allでimportされていると、どれが使われている可能性があるのか?が、import部分を見ただけではわかりにくい
- IDEなどに頼ってコード書くなら、そんなの気にならない派閥もありそう?
- とはいえ、逆にallを禁止して、個別にimportする規約にする、linterを設定しておくとしても、それはそれで、毎回import追加するの面倒だし、慣れないと「どれをimportすればいいのか?」の判断に時間がかかって面倒。また、大量に
import cats.syntax.なんとか._がある状態になったら、結局allを import した方がなんならスッキリというか、かえってわかりやすいのでは???
的な観点もあります。
さて、計測した条件ですが
- この時点のcats最新の2.13.0
- Scala 2.13.16と3.6.4それぞれ
- scalacOptions指定などは特になし
- github actions上で他の条件を出来るだけ同じにして、繰り返しcleanをしてcompileの時間を計測
- 以下のようなコードを大量にそれぞれ生成
def x1_0: Unit = { import cats.syntax.all._ ; List(2).void } def x1_1: Unit = { import cats.syntax.all._ ; (Option(2), Option(3)).mapN(_ + _) } def x1_2: Unit = { import cats.syntax.all._ ; Option(2).mproduct(a => Option(a + 3)) } def x1_3: Unit = { import cats.syntax.all._ ; Option(List(2)).sequence } def x1_4: Unit = { import cats.syntax.all._ ; List(2).maximumOption } def x1_5: Unit = { import cats.syntax.all._ ; List(2).iterateWhile(_ => false) }
def x1_0: Unit = { import cats.syntax.functor._ ; List(2).void } def x1_1: Unit = { import cats.syntax.apply._ ; (Option(2), Option(3)).mapN(_ + _) } def x1_2: Unit = { import cats.syntax.flatMap._ ; Option(2).mproduct(a => Option(a + 3)) } def x1_3: Unit = { import cats.syntax.traverse._ ; Option(List(2)).sequence } def x1_4: Unit = { import cats.syntax.foldable._ ; List(2).maximumOption } def x1_5: Unit = { import cats.syntax.monad._ ; List(2).iterateWhile(_ => false) }
結果
単位は秒。 念のため書いておくと、すごく大量に生成したからこのくらいかかるのであって、上記のコード単体でこんなに時間がかかるわけではない。
Scala 2.13.16、個別import
69 54 52 52 51 51 52 52 53 53
Scala 2.13.16、all
85 73 70 70 71 70 71 70 70 76
Scala 3.6.4、個別import
78 55 51 51 50 52 51 51 51 51
Scala 3.6.4、all
156 134 128 130 136 135 126 121 123 121
まとめ
それぞれ10回計測したが、JVMの温まり考慮して、初回だけ除いた9回の平均と中央値が
- Scala 2 個別
- 平均52.2秒
- 中央値 52秒
- Scala 2 all
- 平均71.2秒
- 中央値70秒
- Scala 3 個別
- 平均51.4秒
- 中央値51秒
- Scala 3 all
- 平均128.2秒
- 中央値128秒
つまり
という結果になりました。
今回の計測用コードのimportのやり方が実際の使い方に近いか?というと、微妙な部分はあるので、もっと色々なパターンで計測したら、さらに違う結果になる可能性はありますが、 allの方が遅くなる傾向があるのは確かなようです。
Scala 2と比較してScala 3の方が遅くなるのはなんなんだ・・・。
とはいえ、10倍や20倍以上遅くなるなら明らかに速度気にする人にとって気をつけてallを避ける規約にした方がいい可能性はありますが、この程度しか遅くならないなら、個人的にはそこまで気にするレベルではないというか、おそらくプロジェクトのcompile遅い問題に困っていたら、ここを最適化する前に他に大量にやることがあると思うので、無闇にやるべきか?というと微妙というか、速度よりも
「どちらのimport方式がそのチーム内にとってわかりやすいか?特に決めずにどちらも許可にするのか?」
をまず重視した方がいい気がしますね。