Scala 3からのWeak Conformanceの廃止とBoxingによるパフォーマンス劣化

タイトルで大体言いたいことを言い切っているのですが、実際問題になることは少ないとはいえ、原理上はそういう問題が起きると思います。

Weak Conformanceについてすごくざっくり説明すると、ScalaでもJavaでも、IntとLong、あるいはIntとDoubleなどは、それぞれsub type関係ではありません。 しかし、Scala 2ではJavaのプリミティブの挙動に合わせて Int から Long などは、暗黙的に自動で変換される、という仕様がありました。

つまり

if(booleanの値) Intの値 else Longの値

としたときの、このif式の型はLongになります。 しかし、Scala 3では廃止されました。

docs.scala-lang.org

Scala 3の場合に上記のif式は Int | Long となります

Welcome to Scala 3.5.1-RC2 (21.0.4, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
                                                                                                                                                        
scala> def x: Int = 2
def x: Int
                                                                                                                                                        
scala> def y: Long = 3
def y: Long
                                                                                                                                                        
scala> if (true) x else y
val res0: Int | Long = 2

さて、このunion typeですが

docs.scala-lang.org

実態としてはjava.lang.Objectになります。 気になる人はjavapしてみましょう。

さて、ここまでの結果から、Scala 2まで Int と Long だったらLongに変換されていたものが、必ずjava.lang.Objectにboxingされるなら、パフォーマンス的には原理上余計な処理が挟まるはずですね???

というわけで、簡単にJMHで計測してみました。

addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7")
scalaVersion := "2.13.14"
// scalaVersion := "3.5.1-RC2"

enablePlugins(JmhPlugin)
package example

import org.openjdk.jmh.annotations.Benchmark

object Main {
  def size: Int = 100
  val intValues: Array[Int] = (1 to size).toArray
  val booleanValues: Array[Boolean] = intValues.map(_ % 2 == 0)
  val longValues: Array[Long] = intValues.map(_.toLong).reverse
}

class Main{
  @Benchmark
  def test1: Array[String] = {
    var i = 0
    val result: Array[String] = new Array[String](Main.size)
    while(i < Main.size) {
      val n = if (Main.booleanValues(i)) Main.intValues(i) else Main.longValues(i)
      result(i) = s"${n} a"
      i += 1
    }
    result
  }
}

Scala 2の結果

[info]   543132.744 ±(99.9%) 6973.003 ops/s [Average]
[info]   (min, avg, max) = (536689.925, 543132.744, 551576.401), stdev = 4612.210
[info]   CI (99.9%): [536159.740, 550105.747] (assumes normal distribution)

Scala 3の結果

[info]   415790.521 ±(99.9%) 3193.212 ops/s [Average]
[info]   (min, avg, max) = (412973.720, 415790.521, 419232.651), stdev = 2112.112
[info]   CI (99.9%): [412597.310, 418983.733] (assumes normal distribution)

はい、数割Scala 2の方が速いです。 これで計測方法あっているはず・・・? 余計なboxingその他が発生しないように、あえてArrayとwhile使って書きました。

該当部分のjavapの結果もそれぞれ貼っておきます。

Scala 2では i2l が呼ばれているのに対して、Scala 3では scala/runtime/BoxesRunTime.boxToIntegerscala/runtime/BoxesRunTime.boxToLong が呼ばれていますね。

StringBuilderのappendのオーバーロードの呼ばれている型も異なります。

確かに今回のように結局Stringに変換するなら、Boxingしてもしなくても、結果は同じなのでバグったりはしないわけですが、こういう違いはありますね!

また、こういうパターンを検知可能なものを作ってあるので、必要な場合は使ってください

xuwei-k.hatenablog.com

Scala 2

  public java.lang.String[] test1();
    descriptor: ()[Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=5, locals=5, args_size=1
         0: iconst_0
         1: istore_1
         2: getstatic     #16                 // Field example/Main$.MODULE$:Lexample/Main$;
         5: invokevirtual #30                 // Method example/Main$.size:()I
         8: anewarray     #35                 // class java/lang/String
        11: astore_2
        12: iload_1
        13: getstatic     #16                 // Field example/Main$.MODULE$:Lexample/Main$;
        16: invokevirtual #30                 // Method example/Main$.size:()I
        19: if_icmpge     84
        22: getstatic     #16                 // Field example/Main$.MODULE$:Lexample/Main$;
        25: invokevirtual #22                 // Method example/Main$.booleanValues:()[Z
        28: iload_1
        29: baload
        30: ifeq          45
        33: getstatic     #16                 // Field example/Main$.MODULE$:Lexample/Main$;
        36: invokevirtual #26                 // Method example/Main$.intValues:()[I
        39: iload_1
        40: iaload
        41: i2l
        42: goto          53
        45: getstatic     #16                 // Field example/Main$.MODULE$:Lexample/Main$;
        48: invokevirtual #18                 // Method example/Main$.longValues:()[J
        51: iload_1
        52: laload
        53: lstore_3
        54: aload_2
        55: iload_1
        56: new           #37                 // class java/lang/StringBuilder
        59: dup
        60: ldc           #38                 // int 2
        62: invokespecial #42                 // Method java/lang/StringBuilder."<init>":(I)V
        65: lload_3
        66: invokevirtual #46                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
        69: ldc           #48                 // String  a
        71: invokevirtual #51                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        74: invokevirtual #55                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        77: aastore
        78: iinc          1, 1
        81: goto          12
        84: aload_2
        85: areturn

Scala 3

  public java.lang.String[] test1();
    descriptor: ()[Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=5, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: getstatic     #13                 // Field example/Main$.MODULE$:Lexample/Main$;
         5: invokevirtual #27                 // Method example/Main$.size:()I
         8: anewarray     #38                 // class java/lang/String
        11: checkcast     #40                 // class "[Ljava/lang/String;"
        14: astore_2
        15: iload_1
        16: getstatic     #13                 // Field example/Main$.MODULE$:Lexample/Main$;
        19: invokevirtual #27                 // Method example/Main$.size:()I
        22: if_icmpge     92
        25: getstatic     #13                 // Field example/Main$.MODULE$:Lexample/Main$;
        28: invokevirtual #15                 // Method example/Main$.booleanValues:()[Z
        31: iload_1
        32: baload
        33: ifeq          50
        36: getstatic     #13                 // Field example/Main$.MODULE$:Lexample/Main$;
        39: invokevirtual #19                 // Method example/Main$.intValues:()[I
        42: iload_1
        43: iaload
        44: invokestatic  #46                 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
        47: goto          61
        50: getstatic     #13                 // Field example/Main$.MODULE$:Lexample/Main$;
        53: invokevirtual #23                 // Method example/Main$.longValues:()[J
        56: iload_1
        57: laload
        58: invokestatic  #50                 // Method scala/runtime/BoxesRunTime.boxToLong:(J)Ljava/lang/Long;
        61: astore_3
        62: aload_2
        63: iload_1
        64: new           #52                 // class java/lang/StringBuilder
        67: dup
        68: ldc           #53                 // int 2
        70: invokespecial #56                 // Method java/lang/StringBuilder."<init>":(I)V
        73: aload_3
        74: invokevirtual #60                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
        77: ldc           #62                 // String  a
        79: invokevirtual #65                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        82: invokevirtual #69                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        85: aastore
        86: iinc          1, 1
        89: goto          15
        92: aload_2
        93: areturn