The Wayback Machine - https://web.archive.org/web/20210109122044/https://github.com/theiterators/kebs/issues/46
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

Check allocation scheme for primivite types with kebs-tagged #46

Closed
luksow opened this issue Oct 8, 2019 · 2 comments
Closed

Check allocation scheme for primivite types with kebs-tagged #46

luksow opened this issue Oct 8, 2019 · 2 comments

Comments

@luksow
Copy link
Contributor

@luksow luksow commented Oct 8, 2019

We need to check if kebs-tagged provides no overhead for primitive type. If there's an overhead, consider changing representation. See: https://github.com/estatico/scala-newtype

@agluszak
Copy link
Contributor

@agluszak agluszak commented Apr 22, 2020

I conducted some research.

TL;DR
Value classes are more efficient than tagged types, using extension methods requires some virtual calls, runtime casting requires boxing and unboxing. Tagged types are at least as good as scala-newtype. Overhead can be reduced by either calling asInstanceOf directly or enabling optimization flags in the compiler

My testing procedure was as follows: I copied the tagging snippet from Kebs and applied various modifications to it. I was inspecting the generated bytecode of a class with a method returning a tagged Integer. I used Scala 2.12.

The code I used was:

trait Tagged[+T, +U]

type @@[T, +U] = T with Tagged[T, U]

implicit class TaggingExtensions[T](val t: T) extends AnyVal {
    def taggedWith[U]: T @@ U = t.asInstanceOf[T @@ U]
}

trait Tag

case class Boxed(i: Int)

case class Unboxed(i: Int) extends AnyVal

class Test {
    def testNormal    = 2
    def testTagged    = 2.taggedWith[Tag]
    def testTaggedRaw = 2.asInstanceOf[Int @@ Tag]
    def testBoxed     = Boxed(2)
    def testUnboxed   = Unboxed(2)
    def testInteger = new Integer(2)
    def testOption = Some(2)
    def testOptionUnboxed = Some(Unboxed(2))
    def testOptionTagged = Some(2.taggedWith[Tag])
}

and the relevant parts of the bytecode were:

public int testNormal();
    Code:
       0: iconst_2
       1: ireturn

  public int testTagged();
    Code:
       0: getstatic     #26                 // Field T$TaggingExtensions$.MODULE$:LT$TaggingExtensions$;
       3: getstatic     #31                 // Field T$.MODULE$:LT$;
       6: iconst_2
       7: invokestatic  #37                 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
      10: invokevirtual #41                 // Method T$.TaggingExtensions:(Ljava/lang/Object;)Ljava/lang/Object;
      13: invokevirtual #44                 // Method T$TaggingExtensions$.taggedWith$extension:(Ljava/lang/Object;)Ljava/lang/Object;
      16: invokestatic  #48                 // Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
      19: ireturn

  public int testTaggedRaw();
    Code:
       0: iconst_2
       1: invokestatic  #37                 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
       4: invokestatic  #48                 // Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
       7: ireturn

  public T$Boxed testBoxed();
    Code:
       0: new           #7                  // class T$Boxed
       3: dup
       4: iconst_2
       5: invokespecial #55                 // Method T$Boxed."<init>":(I)V
       8: areturn

  public int testUnboxed();
    Code:
       0: iconst_2
       1: ireturn

  public java.lang.Integer testInteger();
    Code:
       0: new           #60                 // class java/lang/Integer
       3: dup
       4: iconst_2
       5: invokespecial #61                 // Method java/lang/Integer."<init>":(I)V
       8: areturn

  public scala.Some<java.lang.Object> testOption();
    Code:
       0: new           #66                 // class scala/Some
       3: dup
       4: iconst_2
       5: invokestatic  #37                 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
       8: invokespecial #69                 // Method scala/Some."<init>":(Ljava/lang/Object;)V
      11: areturn

  public scala.Some<T$Unboxed> testOptionUnboxed();
    Code:
       0: new           #66                 // class scala/Some
       3: dup
       4: new           #16                 // class T$Unboxed
       7: dup
       8: iconst_2
       9: invokespecial #72                 // Method T$Unboxed."<init>":(I)V
      12: invokespecial #69                 // Method scala/Some."<init>":(Ljava/lang/Object;)V
      15: areturn

  public scala.Some<java.lang.Object> testOptionTagged();
    Code:
       0: new           #66                 // class scala/Some
       3: dup
       4: getstatic     #26                 // Field T$TaggingExtensions$.MODULE$:LT$TaggingExtensions$;
       7: getstatic     #31                 // Field T$.MODULE$:LT$;
      10: iconst_2
      11: invokestatic  #37                 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
      14: invokevirtual #41                 // Method T$.TaggingExtensions:(Ljava/lang/Object;)Ljava/lang/Object;
      17: invokevirtual #44                 // Method T$TaggingExtensions$.taggedWith$extension:(Ljava/lang/Object;)Ljava/lang/Object;
      20: invokespecial #69                 // Method scala/Some."<init>":(Ljava/lang/Object;)V
      23: areturn


As we can see, the tagging mechanism as currently implemented (called via .taggedWith extension method) results in boxing the integer, then calling a virtual method that creates the TaggingExtensions object, then calling a virtual method taggedWith on it (but since all it does is casting a generic type to a generic type - it's a no-op due to the type erasure) and then unboxing the result.

The important part here is that the returned value is unboxed (just as in scala-newtype when using newsubtype macro). But the sad part is that in the process it gets boxed and unboxed back and forth.

Can we do better?

Our goal is to have the generated bytecode look identical to the "normal" version, i.e. simply returning the int, without any virtual calls and without boxing. It turns out that this is what we get when we use plain old value classes. So yes, probably value classes are more efficient than tagged types.

If we call asInstanceOf directly (i.e. without the extension method) the generated bytecode gets reduced to only 2 static calls responsible for boxing and unboxing (which is obviously redundant, because nothing happens in between).

I looked for ways to reduce the footprint even further. I tried specializing the tagged type for integers (https://scalac.io/specialized-generics-object-instantiation/), making the implicit class final and marking taggedWith method as @inline. None of that worked. It turns out that @inline is only a suggestion for the compiler (in Dotty it'll become a guarantee, but it's a whole different story).

What worked, and what results in (according to my knowledge) the best bytecode we can get, was enabling compiler optimization flags. Here I used what was written on Lightbend's blog. If compiled with flags -opt:l:inline -opt-inline-from:**, the snippet above gives us the following bytecode:

  public int testNormal();
    Code:
       0: iconst_2
       1: ireturn

  public int testTagged();
    Code:
       0: getstatic     #26                 // Field T$TaggingExtensions$.MODULE$:LT$TaggingExtensions$;
       3: pop
       4: getstatic     #31                 // Field T$.MODULE$:LT$;
       7: pop
       8: iconst_2
       9: ireturn

  public int testTaggedRaw();
    Code:
       0: iconst_2
       1: ireturn

  public T$Boxed testBoxed();
    Code:
       0: new           #7                  // class T$Boxed
       3: dup
       4: iconst_2
       5: invokespecial #38                 // Method T$Boxed."<init>":(I)V
       8: areturn

  public int testUnboxed();
    Code:
       0: iconst_2
       1: ireturn

  public java.lang.Integer testInteger();
    Code:
       0: new           #43                 // class java/lang/Integer
       3: dup
       4: iconst_2
       5: invokespecial #44                 // Method java/lang/Integer."<init>":(I)V
       8: areturn

  public scala.Some<java.lang.Object> testOption();
    Code:
       0: new           #49                 // class scala/Some
       3: dup
       4: iconst_2
       5: invokestatic  #53                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       8: invokespecial #56                 // Method scala/Some."<init>":(Ljava/lang/Object;)V
      11: areturn

  public scala.Some<T$Unboxed> testOptionUnboxed();
    Code:
       0: new           #49                 // class scala/Some
       3: dup
       4: new           #16                 // class T$Unboxed
       7: dup
       8: iconst_2
       9: invokespecial #59                 // Method T$Unboxed."<init>":(I)V
      12: invokespecial #56                 // Method scala/Some."<init>":(Ljava/lang/Object;)V
      15: areturn

  public scala.Some<java.lang.Object> testOptionTagged();
    Code:
       0: new           #49                 // class scala/Some
       3: dup
       4: getstatic     #26                 // Field T$TaggingExtensions$.MODULE$:LT$TaggingExtensions$;
       7: pop
       8: getstatic     #31                 // Field T$.MODULE$:LT$;
      11: pop
      12: iconst_2
      13: invokestatic  #53                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      16: invokespecial #56                 // Method scala/Some."<init>":(Ljava/lang/Object;)V
      19: areturn

Now even in the case of the extension method, there are no method calls (either virtual or static)! They got replaced with redundant access to some fields (it's worth checking in the inner workings of Scala's compiler why they are not completely removed...), but AFAIK that's much faster.

So, to sum it up, the way that tagged types are implemented now is fine, but we should enable inlining optimizations in our code (and probably also compile kebs itself with them, as it shrinks the bytecode a bit in case if the end user doesn't enable them)

taggedWith (with inlining)

  public <U> T taggedWith();
    Code:
       0: getstatic     #27                 // Field T$TaggingExtensions$.MODULE$:LT$TaggingExtensions$;
       3: pop
       4: aload_0
       5: invokevirtual #29                 // Method t:()Ljava/lang/Object;
       8: areturn

taggedWith (without inlining)

  public <U> T taggedWith();
    Code:
       0: getstatic     #27                 // Field T$TaggingExtensions$.MODULE$:LT$TaggingExtensions$;
       3: aload_0
       4: invokevirtual #29                 // Method t:()Ljava/lang/Object;
       7: invokevirtual #33                 // Method T$TaggingExtensions$.taggedWith$extension:(Ljava/lang/Object;)Ljava/lang/Object;
      10: areturn

where taggedWith$extension is a no-op

  public final <U, T> T taggedWith$extension(T);
    Code:
       0: aload_1
       1: areturn
@luksow
Copy link
Contributor Author

@luksow luksow commented Apr 22, 2020

Many thanks @agluszak!

@luksow luksow closed this Apr 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
2 participants
You can’t perform that action at this time.