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