I see the change has a few feature toggles under -XX:+UnlockDiagnosticVMOptions. That will make it easier to test if the workload is sensitive to this change, thanks!
❯ java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal | grep Second
bool ErrorLogSecondaryErrorDetails = false {diagnostic} {default}
bool InlineSecondarySupersTest = true {C2 diagnostic} {default}
bool StressSecondarySupers = false {diagnostic} {default}
bool UseSecondarySupersCache = true {diagnostic} {default}
bool UseSecondarySupersTable = true {diagnostic} {default}
bool VerifySecondarySupers = false {diagnostic} {default}
I’ve also analyzed the longest secondary_super lists in the Scala 2 compiler implementation. There are indeed some long ones due to the use of the Cake Pattern. The Global class that ties all the components together has more than 64 transitive interfaces.
Another language feature (self types) results in synthetic checkcast of from Global to an interface among this long list.
Boiled down:
scala> trait T1 { self: Logging => def m = log("foo") }; trait Logging { def log(a: Any) = ()}; class Global extends Logging with T1 /* and many more */;
trait T1
trait Logging
class Global
scala> :javap T1#m
public default void m();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: checkcast #12 // class $line7/$read$$iw$Logging
4: ldc #24 // String foo
6: invokeinterface #28, 2 // InterfaceMethod $line7/$read$$iw$Logging.log:(Ljava/lang/Object;)V
11: return
I’m modifying GitHub - RedHatPerf/type-pollution-agent to profile such type checks over long secondary_super lists to try to pin them down to hot methods in the compiler.
As I understand from your comments and code, >64 will fall into:
// FIXME: We could do something smarter here, maybe a vectorized
// comparison or a binary search, but is that worth any added
// complexity?
And 25-64 will pay some increasing extra cost for probing on collisions.
Unfortunately the ByteBuddy transform in the agent is corrupting the compiler so it fails with an access error before it starts real work.
But some code is run which produces the trace:
--------------------------
Excessive Secondary Supers:
--------------------------
1: scala.tools.nsc.Global
Count: 46351
Types:
scala.tools.nsc.Global
Traces:
scala.reflect.internal.Names$TermName.scala$reflect$internal$Names$TermName$$$outer(Names.scala:570)
class: scala.tools.nsc.Global
count: 31138
scala.reflect.internal.StdAttachments$Attachable.$init$(StdAttachments.scala:24)
class: scala.tools.nsc.Global
count: 7932
scala.reflect.internal.Names$TypeName.scala$reflect$internal$Names$TypeName$$$outer(Names.scala:612)
class: scala.tools.nsc.Global
count: 6852
scala.reflect.internal.Names$TermName$.scala$reflect$internal$Names$TermName$$$outer(Names.scala:607)
class: scala.tools.nsc.Global
count: 121
...
Update: I’m now fairy certain that Scala 2 compiler is about 5% slower due do this change. This seems to be worked around by -XX:+UnlockDiagnosticVMOptions -XX:-UseSecondarySupersTable.
Here’s a standalone, Java implemented benchmark that shows the essential pattern of code in scalac that results in hot-path checkcast:
Scala 3 implementation uses a different overall design pattern so doesn’t have classes with such long transitive interface lists, and as such is unaffected.