Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Universal traits shouldn't add allocation to a value class #9731

Open
scabug opened this issue Apr 1, 2016 · 8 comments
Open

Universal traits shouldn't add allocation to a value class #9731

scabug opened this issue Apr 1, 2016 · 8 comments
Assignees
Milestone

Comments

@scabug
Copy link

scabug commented Apr 1, 2016

Given

trait StringVal extends Any { def value: String ; final override def toString = value }

final case class Foo(value: String) extends AnyVal with StringVal
final case class Bar(value: String) extends AnyVal { override def toString = value }

def foo(x: Foo) = println(x.toString)
def bar(x: Bar) = println(x.toString)

javap shows a difference in bytecode:

  public void foo(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=2, args_size=2
         0: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
         3: new           #21                 // class Foo
         6: dup
         7: aload_1
         8: invokespecial #23                 // Method Foo."<init>":(Ljava/lang/String;)V
        11: invokevirtual #27                 // Method Foo.toString:()Ljava/lang/String;
        14: invokevirtual #31                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
        17: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  this   L;
            0      18     1     x   Ljava/lang/String;
      LineNumberTable:
        line 14: 0

  public void bar(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
         3: getstatic     #24                 // Field Bar$.MODULE$:LBar$;
         6: aload_1
         7: invokevirtual #28                 // Method Bar$.toString$extension:(Ljava/lang/String;)Ljava/lang/String;
        10: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
        13: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  this   L;
            0      14     1     x   Ljava/lang/String;
      LineNumberTable:
        line 13: 0

For more context see https://gitter.im/scala/scala?at=56fd99f0d9b73e635f6823bb where I discovered this with @Ichoran.

(meta: Unsure if to classify this as a bug or an improvement, I've opted for bug)
(meta: Also unsure how best to detail this ticket in the summary)

@scabug
Copy link
Author

scabug commented Apr 1, 2016

Imported From: https://issues.scala-lang.org/browse/SI-9731?orig=1
Reporter: @dwijnand
Affected Versions: 2.11.8

@scabug
Copy link
Author

scabug commented Apr 1, 2016

@lrytz said:
i'll take a look, maybe we can do something, either in the frontend or in the optimizer. it would definitely be a valuable improvement for 2.12.

@scabug
Copy link
Author

scabug commented Apr 13, 2016

@lrytz said:
Let me quickly outline why this is not straightforward

trait T extends Any {
  def s: String
  def t1 = s.toUpperCase
}

This universal trait needs to be compiled into a valid interface. We don't know that s is potentially implemented as a value class field - in fact, T can be implemented by a non-value class. So the bytecode is

public abstract interface T {
  public abstract s()Ljava/lang/String;

  public default t1()Ljava/lang/String;
    ALOAD 0
    INVOKEINTERFACE T.s ()Ljava/lang/String;
    INVOKEVIRTUAL java/lang/String.toUpperCase ()Ljava/lang/String;
    ARETURN
}

Now we introduce a value class

class C(val s: String) extends AnyVal with T

If we don't want to break separate compilation (change the body of T.t1, re-compile T, also C should get the new impl) then we have no choice but to invoke T.t1, which requires an instance.

Now there's a few possibilities. We can get actually already quite close just with inlining, but it (currently, 2.12.0-M4) requires an @inline annotation:

trait T extends Any {
  def s: String
  @inline final def t1 = s.toUpperCase
}
class C(val s: String) extends AnyVal with T
class A { def f1(c: C) = c.t1 }

gives

public final class C implements T  {
  public f1(Ljava/lang/String;)Ljava/lang/String;
    NEW C
    DUP
    ALOAD 1
    INVOKESPECIAL C.<init> (Ljava/lang/String;)V
    INVOKEINTERFACE T.s ()Ljava/lang/String;
    INVOKEVIRTUAL java/lang/String.toUpperCase ()Ljava/lang/String;
    ARETURN

We're planning to extend our box-unbox optimization to value classes (currently supports tuples and primitive boxes), which would eliminate the allocation. This will probably not happen for 2.12.0, unfortunately - there's not much time left. Note that the JVM's escape analysis will probably also eliminate the box.

Other options I see:

  • we could introduce some marker annotation to tell the compiler that def s: String is potentially a value class field. Then we could split up methods that access s into a static method that gets the string as parameter and the instance method that invokes s() and passes it to the static method. the value class could then invoke the static method.
  • when generating the value class code, we could locate the bytecode of methods inherited from universal traits and duplicate it, and attempt to create static copies, replacing accesses to this.s() by the value class field. that sounds a bit complicated though, it would have to happen during the optimizer where we can read and duplicate bytecode.

the second of this ideas is related to inlining (code is duplicated), also related to what dimitry is researching for dotty (creating specialized copies of methods).

@scabug
Copy link
Author

scabug commented Apr 13, 2016

@lrytz said:
i think the most likely thing that will happen is an improved optimzier that eliminates value class boxes, together with improved inlining heuristics that will inline value class calls in case a box can be eliminated. this was already on my schedule, but after 2.12.0 is out.

@scabug
Copy link
Author

scabug commented Apr 13, 2016

@lrytz said:
cc @DarkDimius - i just read on the gitter channel that "Dotty is trying a design that eliminates the allocation" - can you summarize?

@scabug
Copy link
Author

scabug commented Apr 25, 2016

@DarkDimius said (edited on Apr 26, 2016 8:37:01 AM UTC):
The basic idea was that universal traits will undergo the same transformation as value classes:

trait StringVal extends Any { def value: String ; final override def toString = value }

becomes

trait StringVal extends Any { def value: String ; final override def toString = StringVal$.toString_extension(this)  }
object StringVal { final override def toString_extension[T <: StringVal](t: T): String = t.value }

This would allow to pass a value class to the value class to the extension method.
Then, I'd have a run of specialization(likely linker-driven on-demand), that would rewrite the code to take a StringVal.

object StringVal { // specialized
  final override def toString_extension[T <: StringVal](t: T): String = t.value 
  final override def toString_extension_spec$1[T = Foo](t: T): String = t.value 
}

This would mean that the normal boxing elimination of value classes would kick-in.

object StringVal {  // erased
  final override def toString_extension(t: StringVal): String = t.value 
  final override def toString_extension_spec$1(t: String): String = Foo$.value_extension(t)
}

@scabug
Copy link
Author

scabug commented Apr 6, 2017

David Bouyssie (crumbpicker) said:
In the contributors mailing list I made a suggestion about the possible usage of "sealed traits" to define inheritable extension methods:
https://contributors.scala-lang.org/t/value-classes-and-universal-traits/

This would be a strong requirement but I think it could help to know the list of possible implementations of the universal trait.

@scabug scabug added this to the Backlog milestone Apr 7, 2017
@david-bouyssie
Copy link

Sorry for the late reply.

@lrytz: I understand that it's not possible to anticipate the nature of the different implementations of a given universal trait. But my question was relative to the use of "sealed universal traits". In this context we can determine exactly the list of implementations.
Don't you think it may be helpful regarding the inlining operation?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants