タイトルで大体言いたいことを言い切っているのですが、実際問題になることは少ないとはいえ、原理上はそういう問題が起きると思います。
Weak Conformanceについてすごくざっくり説明すると、ScalaでもJavaでも、IntとLong、あるいはIntとDoubleなどは、それぞれsub type関係ではありません。 しかし、Scala 2ではJavaのプリミティブの挙動に合わせて Int から Long などは、暗黙的に自動で変換される、という仕様がありました。
つまり
if(booleanの値) Intの値 else Longの値
としたときの、このif式の型はLongになります。 しかし、Scala 3では廃止されました。
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ですが
実態としては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.boxToInteger
や scala/runtime/BoxesRunTime.boxToLong
が呼ばれていますね。
StringBuilderのappendのオーバーロードの呼ばれている型も異なります。
確かに今回のように結局Stringに変換するなら、Boxingしてもしなくても、結果は同じなのでバグったりはしないわけですが、こういう違いはありますね!
また、こういうパターンを検知可能なものを作ってあるので、必要な場合は使ってください
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