Avoiding boxing/unboxing within function
Asked Answered
O

3

14

For a numeric intensive code I have written a function with the following signature:

def update( f: (Int,Int,Double) => Double ): Unit = {...}

However, because Function3 is not specialized, every application of f results in boxing/unboxing the 3 arguments and the result type.

I could use a special updater class:

trait Updater {
  def apply( i: Int, j: Int, x: Double ): Double
}
def update( f: Updater ): Unit = {...}

But the invocation is cumbersome (and java-ish):

//with function
b.update( (i,j,_) => if( i==0 || j ==0 ) 1.0 else 0.5 )

//with updater
b.update( new Updater {
  def apply( i: Int, j: Int, x: Double ) = if( i==0 || j ==0 ) 1.0 else 0.5
} )

Is there a way to avoid boxing/unboxing while still using the lambda syntax ? I was hoping macros will help, but I cannot figure any solution.

EDIT: I analyzed the function3 generated byte code with javap. An unboxed method is generated by the compiler, along the generic method (see below). Is there a way to call the unboxed one directly ?

public final double apply(int, int, double);
  Code:
   0:   ldc2_w  #14; //double 100.0d
   3:   iload_2
   4:   i2d
   5:   dmul
   6:   iload_1
   7:   i2d
   8:   ddiv
   9:   dreturn

public final java.lang.Object apply(java.lang.Object, java.lang.Object, java.lang.Object);
  Code:
   0:   aload_0
   1:   aload_1
   2:   invokestatic    #31; //Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
   5:   aload_2
   6:   invokestatic    #31; //Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
   9:   aload_3
   10:  invokestatic    #35; //Method scala/runtime/BoxesRunTime.unboxToDouble:(Ljava/lang/Object;)D
   13:  invokevirtual   #37; //Method apply:(IID)D
   16:  invokestatic    #41; //Method scala/runtime/BoxesRunTime.boxToDouble:(D)Ljava/lang/Double;
   19:  areturn
Overarm answered 23/8, 2012 at 8:4 Comment(0)
C
6

Since you mentioned macros as a possible solution, I got the idea of writing a macro that takes an anonymous function, extracts the apply methods and inserts it into an anonymous class that extends a custom function trait called F3. This is the quite long implementation.

The trait F3

trait F3[@specialized A, @specialized B, @specialized C, @specialized D] {
  def apply(a:A, b:B, c:C):D
}

The macro

  implicit def function3toF3[A,B,C,D](f:Function3[A,B,C,D]):F3[A,B,C,D] = macro impl[A,B,C,D]

  def impl[A,B,C,D](c:Context)(f:c.Expr[Function3[A,B,C,D]]):c.Expr[F3[A,B,C,D]] = {
    import c.universe._
    var Function(args,body) = f.tree
    args = args.map(c.resetAllAttrs(_).asInstanceOf[ValDef])
    body = c.resetAllAttrs(body)
    val res = 
      Block(
        List(
          ClassDef(
            Modifiers(Flag.FINAL),
            newTypeName("$anon"),
            List(),
            Template(
              List(
                AppliedTypeTree(Ident(c.mirror.staticClass("mcro.F3")),
                  List(
                    Ident(c.mirror.staticClass("scala.Int")),
                    Ident(c.mirror.staticClass("scala.Int")),
                    Ident(c.mirror.staticClass("scala.Double")),
                    Ident(c.mirror.staticClass("scala.Double"))
                  )
                )
              ),
              emptyValDef,
              List(
                DefDef(
                  Modifiers(),
                  nme.CONSTRUCTOR,
                  List(),
                  List(
                    List()
                  ),
                  TypeTree(),
                  Block(
                    List(
                      Apply(
                        Select(Super(This(newTypeName("")), newTypeName("")), newTermName("<init>")),
                        List()
                      )
                    ),
                    Literal(Constant(()))
                  )
                ),
                DefDef(
                  Modifiers(Flag.OVERRIDE),
                  newTermName("apply"),
                  List(),
                  List(args),
                  TypeTree(),
                  body
                )
              )
            )
          )
        ),
        Apply(
          Select(
            New(
              Ident(newTypeName("$anon"))
            ),
            nme.CONSTRUCTOR
          ),
          List()
        )
      )




    c.Expr[F3[A,B,C,D]](res)
  }

Since I defined the macro as implicit, it can be used like this:

def foo(f:F3[Int,Int,Double,Double]) = {
  println(f.apply(1,2,3))
}

foo((a:Int,b:Int,c:Double)=>a+b+c)

Before foo is called, the macro is invoked because foo expects an instance of F3. As expected, the call to foo prints "6.0". Now let's look at the disassembly of the foo method, to make sure that no boxing/unboxing takes place:

public void foo(mcro.F3);
  Code:
   Stack=6, Locals=2, Args_size=2
   0:   getstatic       #19; //Field scala/Predef$.MODULE$:Lscala/Predef$;
   3:   aload_1
   4:   iconst_1
   5:   iconst_2
   6:   ldc2_w  #20; //double 3.0d
   9:   invokeinterface #27,  5; //InterfaceMethod mcro/F3.apply$mcIIDD$sp:(IID)D
   14:  invokestatic    #33; //Method scala/runtime/BoxesRunTime.boxToDouble:(D)Ljava/lang/Double;
   17:  invokevirtual   #37; //Method scala/Predef$.println:(Ljava/lang/Object;)V
   20:  return

The only boxing that is done here is for the call to println. Yay!

One last remark: In its current state, the macro only works for the special case of Int,Int,Double,Double but that can easily be fixed. I leave that as an exercise to the reader.

Coo answered 23/8, 2012 at 15:58 Comment(5)
A specialized version of Function3 generate 700+ class files for 25kb of bytecode. Your macro is a really good start to solve this kind of problem, but think you can drop the @specialized.Lactose
you can reduce the number of classes generated by specifying for which types the trait should be specialized. If it is only Int and Doublle, you will only get 2 ^^ 4 = 16 class files. Besides, those additional class files won't hurt you in any way, as they are never loaded.Coo
How would you drop the @specialized? Avoiding boxing is the whole point.Coo
It was just a thought, I have to test it. Since you use the override, I think you don't need @specialized to avoid boxing, maybe I am wrong.Lactose
What about reducing the scope of @specialized? As, for example, in... Function1Parmer
F
2

About the anonymous class which extends Function3 (the bytecode of which you show) generated by Scalac - it is not possible to call the overloaded apply with primitive parameters from within the b.update, because that update method takes a Function3, which does not have an apply with primitive parameters.

From within the Function3 bytecode, the only apply is:

public abstract java.lang.Object apply(java.lang.Object, java.lang.Object, java.lang.Object);

You could instead use Function2[Long, Double], which is specialized on those types, and encode 2 integers x and y in a long as (x.toLong << 32) + y, and decode them as v & 0xffffffff and (v >> 32) & 0xffffffff.

Flex answered 23/8, 2012 at 9:31 Comment(2)
Nice suggestion. But i will still need to rely on boiler plate code to convert/unconvert everything by hand.Overarm
The precedence of << is evil, x.toLong << 32 + y is in fact x.toLong << (32 + y). Use more parentheses.Stanwin
P
0

As Function1 is specialized, a possible solution is to use currying and change your update method to:

def update( f: Int => Int => Double => Double ): Unit = {...}

and change the inlined function accordingly. with your example (update was slightly modified to test it quickly):

scala> def update( f: Int => Int => Double => Double ): Double = f(1)(2)(3.0)
update: (f: Int => (Int => (Double => Double)))Double

scala> update(i => j => _ => if (i == 0 && j == 0) 1.0 else 0.5)
res1: Double = 0.5

Edit: Ase explained in the comments, it doesn't completely help since the first parameter is still boxed. I leave the answer to keep a trace about it.

Parmer answered 23/8, 2012 at 9:44 Comment(6)
How do you declare "apply( i: Int, j: Int, x: Double ) = if( i==0 || j ==0 ) 1.0 else 0.5" ?Lactose
Intersting, but I decompiled the update method and it still boxes the first Int parameter to get the next function.Overarm
That's because the return type of the first function is not a primitive type, but an object, hence, the function is not specialized.Flex
Interesting. So to be specialized, all the type parameters must be specializable. I didn't know it, thanks.Parmer
Another thing about @specialized, a specialized version of Function3 generate 700+ class files for 25kb of bytecode.Lactose
@Lactose In this case I just need an (Int,Int,Double,Double) specialization and not all the combinations.Overarm

© 2022 - 2024 — McMap. All rights reserved.