Caching lambda proxy classes

Peter Levart peter.levart at gmail.com
Tue Mar 12 10:00:52 PDT 2013


Hi,

The result of my caching experimentation is the following code:

http://dl.dropbox.com/u/101777488/jdk8-lambda/InnerClassLambdaMetafactory/webrev.02/index.html

The changes are quite involving, I admit. The strategy for caching only 
tries to attempt caching if there might be more than one capturing site 
for same target implementation method, that is:
- if the proxy class is serializable or
- if the target method is not a synthetic method generated by javac from 
lambda body, but a method from a method reference.

For normal non-serializable lambdas,  *exactly one* capturing site is 
guaranteed per target method and caching is not attempted so there's no 
overhead for them.

The involving change is the proxy class generation logic which decides 
what the outer class and class-loader for generated proxy inner class is 
going to be.

If all of the following is true:
- proxy class is not serializable (since $deserializeLambda$ method must 
be somewhere otherwise) and
- caching is attempted and
- all the types referenced from the proxy class are visible by the 
class-loader of the target implementation method's declaring class

...then outer class is chosen to be the target implementation method's 
declaring class and the class-loader for defining the proxy class is 
chosen to be it's class-loader. Otherwise the outer class and 
class-loader are chosen to be the lambda capturing class and 
class-loader, respectively.

In either case, if caching is attempted, chosen outer class is the 
"host" for cached proxy classes. In case when the "host" class is chosen 
to be the target implementation method's declaring class, effectivity of 
caching is much larger. For example, the following test program (method 
bodies of A.test1(), A.test2(), B.test1() and B.test2() are identical):

package test;

import java.io.*;
import java.util.function.IntBinaryOperator;

public class LambdaTest {

     interface MyIntBinaryOperator extends IntBinaryOperator {
     }

     static class A {
         static void test1() {
             System.out.println("\nA.test1()\n");

             IntBinaryOperator methRef = Math::min;
             System.out.println("       methRef: " + methRef);

             IntBinaryOperator serMethRef = (IntBinaryOperator & 
Serializable) Math::min;
             System.out.println("    serMethRef: " + serMethRef);

             IntBinaryOperator deserMethRef = 
deserialize(serialize(serMethRef));
             System.out.println("  deserMethRef: " + deserMethRef);

             MyIntBinaryOperator myMethRef = Math::min;
             System.out.println("     myMethRef: " + myMethRef);

             IntBinaryOperator lambda = (a, b) -> a <= b ? a : b;
             System.out.println("        lambda: " + lambda);

             IntBinaryOperator serLambda = (IntBinaryOperator & 
Serializable) (a, b) -> a <= b ? a : b;
             System.out.println("     serLambda: " + serLambda);

             IntBinaryOperator deserLambda = 
deserialize(serialize(serLambda));
             System.out.println("   deserLambda: " + deserLambda);
         }

         static void test2() {
             System.out.println("\nA.test2()\n");

             IntBinaryOperator methRef = Math::min;
             System.out.println("       methRef: " + methRef);

             IntBinaryOperator serMethRef = (IntBinaryOperator & 
Serializable) Math::min;
             System.out.println("    serMethRef: " + serMethRef);

             IntBinaryOperator deserMethRef = 
deserialize(serialize(serMethRef));
             System.out.println("  deserMethRef: " + deserMethRef);

             MyIntBinaryOperator myMethRef = Math::min;
             System.out.println("     myMethRef: " + myMethRef);

             IntBinaryOperator lambda = (a, b) -> a <= b ? a : b;
             System.out.println("        lambda: " + lambda);

             IntBinaryOperator serLambda = (IntBinaryOperator & 
Serializable) (a, b) -> a <= b ? a : b;
             System.out.println("     serLambda: " + serLambda);

             IntBinaryOperator deserLambda = 
deserialize(serialize(serLambda));
             System.out.println("   deserLambda: " + deserLambda);
         }
     }

     static class B {
         static void test1() {
             System.out.println("\nB.test1()\n");

             IntBinaryOperator methRef = Math::min;
             System.out.println("       methRef: " + methRef);

             IntBinaryOperator serMethRef = (IntBinaryOperator & 
Serializable) Math::min;
             System.out.println("    serMethRef: " + serMethRef);

             IntBinaryOperator deserMethRef = 
deserialize(serialize(serMethRef));
             System.out.println("  deserMethRef: " + deserMethRef);

             MyIntBinaryOperator myMethRef = Math::min;
             System.out.println("     myMethRef: " + myMethRef);

             IntBinaryOperator lambda = (a, b) -> a <= b ? a : b;
             System.out.println("        lambda: " + lambda);

             IntBinaryOperator serLambda = (IntBinaryOperator & 
Serializable) (a, b) -> a <= b ? a : b;
             System.out.println("     serLambda: " + serLambda);

             IntBinaryOperator deserLambda = 
deserialize(serialize(serLambda));
             System.out.println("   deserLambda: " + deserLambda);
         }

         static void test2() {
             System.out.println("\nB.test2()\n");

             IntBinaryOperator methRef = Math::min;
             System.out.println("       methRef: " + methRef);

             IntBinaryOperator serMethRef = (IntBinaryOperator & 
Serializable) Math::min;
             System.out.println("    serMethRef: " + serMethRef);

             IntBinaryOperator deserMethRef = 
deserialize(serialize(serMethRef));
             System.out.println("  deserMethRef: " + deserMethRef);

             MyIntBinaryOperator myMethRef = Math::min;
             System.out.println("     myMethRef: " + myMethRef);

             IntBinaryOperator lambda = (a, b) -> a <= b ? a : b;
             System.out.println("        lambda: " + lambda);

             IntBinaryOperator serLambda = (IntBinaryOperator & 
Serializable) (a, b) -> a <= b ? a : b;
             System.out.println("     serLambda: " + serLambda);

             IntBinaryOperator deserLambda = 
deserialize(serialize(serLambda));
             System.out.println("   deserLambda: " + deserLambda);
         }
     }

     static byte[] serialize(Object o) {
         ByteArrayOutputStream baos;
         try (
                 ObjectOutputStream oos =
                         new ObjectOutputStream(baos = new 
ByteArrayOutputStream())
         ) {
             oos.writeObject(o);
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
         return baos.toByteArray();
     }

     static <T> T deserialize(byte[] bytes) {
         try (
                 ObjectInputStream ois =
                         new ObjectInputStream(new 
ByteArrayInputStream(bytes))
         ) {
             return (T) ois.readObject();
         } catch (IOException | ClassNotFoundException e) {
             throw new RuntimeException(e);
         }
     }

     public static void main(String[] args) {
         A.test1();
         B.test1();
         A.test2();
         B.test2();
         A.test1();
         B.test1();
     }
}



Prints the following when run with unchanged InnerClassLambdaMetafactory:


A.test1()

        methRef: test.LambdaTest$A$$Lambda$1 at 2d98a335
     serMethRef: test.LambdaTest$A$$Lambda$2 at 4f3f5b24
   deserMethRef: test.LambdaTest$A$$Lambda$3 at 52cc8049
      myMethRef: test.LambdaTest$A$$Lambda$4 at 312b1dae
         lambda: test.LambdaTest$A$$Lambda$5 at 7530d0a
      serLambda: test.LambdaTest$A$$Lambda$6 at 27bc2616
    deserLambda: test.LambdaTest$A$$Lambda$7 at 3d494fbf

B.test1()

        methRef: test.LambdaTest$B$$Lambda$8 at 1ddc4ec2
     serMethRef: test.LambdaTest$B$$Lambda$9 at 133314b
   deserMethRef: test.LambdaTest$B$$Lambda$10 at 70177ecd
      myMethRef: test.LambdaTest$B$$Lambda$11 at 1e80bfe8
         lambda: test.LambdaTest$B$$Lambda$12 at 66a29884
      serLambda: test.LambdaTest$B$$Lambda$13 at 4769b07b
    deserLambda: test.LambdaTest$B$$Lambda$14 at 6f539caf

A.test2()

        methRef: test.LambdaTest$A$$Lambda$15 at 79fc0f2f
     serMethRef: test.LambdaTest$A$$Lambda$16 at 50040f0c
   deserMethRef: test.LambdaTest$A$$Lambda$3 at 52cc8049
      myMethRef: test.LambdaTest$A$$Lambda$17 at 378fd1ac
         lambda: test.LambdaTest$A$$Lambda$18 at 49097b5d
      serLambda: test.LambdaTest$A$$Lambda$19 at 6e2c634b
    deserLambda: test.LambdaTest$A$$Lambda$20 at 7eda2dbb

B.test2()

        methRef: test.LambdaTest$B$$Lambda$21 at 4d405ef7
     serMethRef: test.LambdaTest$B$$Lambda$22 at 6193b845
   deserMethRef: test.LambdaTest$B$$Lambda$10 at 70177ecd
      myMethRef: test.LambdaTest$B$$Lambda$23 at 3f91beef
         lambda: test.LambdaTest$B$$Lambda$24 at 1a6c5a9e
      serLambda: test.LambdaTest$B$$Lambda$25 at 37bba400
    deserLambda: test.LambdaTest$B$$Lambda$26 at 37f8bb67

A.test1()

        methRef: test.LambdaTest$A$$Lambda$1 at 2d98a335
     serMethRef: test.LambdaTest$A$$Lambda$2 at 4f3f5b24
   deserMethRef: test.LambdaTest$A$$Lambda$3 at 52cc8049
      myMethRef: test.LambdaTest$A$$Lambda$4 at 312b1dae
         lambda: test.LambdaTest$A$$Lambda$5 at 7530d0a
      serLambda: test.LambdaTest$A$$Lambda$6 at 27bc2616
    deserLambda: test.LambdaTest$A$$Lambda$7 at 3d494fbf

B.test1()

        methRef: test.LambdaTest$B$$Lambda$8 at 1ddc4ec2
     serMethRef: test.LambdaTest$B$$Lambda$9 at 133314b
   deserMethRef: test.LambdaTest$B$$Lambda$10 at 70177ecd
      myMethRef: test.LambdaTest$B$$Lambda$11 at 1e80bfe8
         lambda: test.LambdaTest$B$$Lambda$12 at 66a29884
      serLambda: test.LambdaTest$B$$Lambda$13 at 4769b07b
    deserLambda: test.LambdaTest$B$$Lambda$14 at 6f539caf



...and the following when run with patched InnerClassLambdaMetafactory:


A.test1()

        methRef: java.lang.Math$$Lambda$1 at 2d98a335
     serMethRef: test.LambdaTest$A$$Lambda$2 at 4f3f5b24
   deserMethRef: test.LambdaTest$A$$Lambda$2 at 5b6f7412
      myMethRef: test.LambdaTest$A$$Lambda$3 at 7530d0a
         lambda: test.LambdaTest$A$$Lambda$4 at 27bc2616
      serLambda: test.LambdaTest$A$$Lambda$5 at 3941a79c
    deserLambda: test.LambdaTest$A$$Lambda$5 at 133314b

B.test1()

        methRef: java.lang.Math$$Lambda$1 at b1bc7ed
     serMethRef: test.LambdaTest$B$$Lambda$6 at 7cd84586
   deserMethRef: test.LambdaTest$B$$Lambda$6 at 66a29884
      myMethRef: test.LambdaTest$B$$Lambda$7 at 4769b07b
         lambda: test.LambdaTest$B$$Lambda$8 at cc34f4d
      serLambda: test.LambdaTest$B$$Lambda$9 at 17a7cec2
    deserLambda: test.LambdaTest$B$$Lambda$9 at 2dda6444

A.test2()

        methRef: java.lang.Math$$Lambda$1 at 5e9f23b4
     serMethRef: test.LambdaTest$A$$Lambda$2 at 4783da3f
   deserMethRef: test.LambdaTest$A$$Lambda$2 at 5b6f7412
      myMethRef: test.LambdaTest$A$$Lambda$3 at 6e2c634b
         lambda: test.LambdaTest$A$$Lambda$10 at 37a71e93
      serLambda: test.LambdaTest$A$$Lambda$11 at 7e6cbb7a
    deserLambda: test.LambdaTest$A$$Lambda$11 at 76fb509a

B.test2()

        methRef: java.lang.Math$$Lambda$1 at 2e817b38
     serMethRef: test.LambdaTest$B$$Lambda$6 at c4437c4
   deserMethRef: test.LambdaTest$B$$Lambda$6 at 66a29884
      myMethRef: test.LambdaTest$B$$Lambda$7 at 1a6c5a9e
         lambda: test.LambdaTest$B$$Lambda$12 at 37bba400
      serLambda: test.LambdaTest$B$$Lambda$13 at 179d3b25
    deserLambda: test.LambdaTest$B$$Lambda$13 at 49c2faae

A.test1()

        methRef: java.lang.Math$$Lambda$1 at 2d98a335
     serMethRef: test.LambdaTest$A$$Lambda$2 at 4f3f5b24
   deserMethRef: test.LambdaTest$A$$Lambda$2 at 5b6f7412
      myMethRef: test.LambdaTest$A$$Lambda$3 at 7530d0a
         lambda: test.LambdaTest$A$$Lambda$4 at 27bc2616
      serLambda: test.LambdaTest$A$$Lambda$5 at 3941a79c
    deserLambda: test.LambdaTest$A$$Lambda$5 at 133314b

B.test1()

        methRef: java.lang.Math$$Lambda$1 at b1bc7ed
     serMethRef: test.LambdaTest$B$$Lambda$6 at 7cd84586
   deserMethRef: test.LambdaTest$B$$Lambda$6 at 66a29884
      myMethRef: test.LambdaTest$B$$Lambda$7 at 4769b07b
         lambda: test.LambdaTest$B$$Lambda$8 at cc34f4d
      serLambda: test.LambdaTest$B$$Lambda$9 at 17a7cec2
    deserLambda: test.LambdaTest$B$$Lambda$9 at 2dda6444


Further improvements/modifications possible:

- when caching for constant method references, instead of caching only 
the class, the whole CallSite object could be cached so not only the 
class but also the instance would be a singleton in the whole JVM for 
each distinct pairs of (target SAM type, source method reference). That 
is, every statement like the following anywhere in the JVM:

     IntBinaryOperator methRef = Math::min;

would evaluate the single IntBinaryOperator instance.

- if caching for serializable method references/lambdas does not prove 
to be effective, the strategy could be modified to not attempt caching 
just because proxy class is serializable. Caching for method references 
that can be hosted on the target method's declaring class is very 
effective though.


What do you think?

A question: What's so special in the $deserializeLambda$ method that it 
has to be generated in each capturing class? The MethodHandles.Lookup?

Regards, Peter


On 03/11/2013 07:07 PM, Peter Levart wrote:
> On 03/10/2013 11:35 PM, Brian Goetz wrote:
>
>>
>> The question is whether the memory usage overhead of the cache and 
>> key objects makes up for the decreased class generation. Have you 
>> done any measurements that would help determine what the breakeven 
>> cache hit rate is?
>
> Not at first, but now that I did, I saw that it was a huge overhead. I 
> tried to minimize this overhead on 3 fronts:
>
> - instead of keeping references to MethodhandleInfo and MethodType 
> objects in the cache keys, which also retained unneeded deeply-nested 
> private structures in those objects, I rather extracted symbolic names 
> (Class and method names) from those objects and dumped them into a 
> single Sintrng[] array (using nulls as delimiters between 
> variable-sized sub-sequences). As it happens, all those Strings are 
> already interned, so the overhead is just the length of the array that 
> way.
>
> - instead of referencing CallSite objects as the values in cache, 
> which also retain unneeded deeply-nested private structures, I rather 
> cached just j.l.Class objects representing proxy classes and 
> re-compute CallSite each time called.
>
> - instead of using ClassValue as a means to maintain per-class caches, 
> I experimented with a plain ConcurrentHashMap referenced directly from 
> the j.l.Class object (with new field added to j.l.Class). Such 
> platform-internal "Class.privateMap" could be used also for other 
> purposes inside JDK since it's about 4x more space-efficient than 
> ClassValue approach (there are a lot of places in JDK that maintain 
> per-class associations using WeakHashMap<Class, ...> wrapped into a 
> synchronized Map, which is not very scalable - scalability issues for 
> Proxy.isProxyClass and Proxy.getProxyClass and others have already 
> been reported some time ago)...
>
> With all 3 optimizations combined, I managed to so squeeze the 
> overhead to 232 bytes for 1st cached class and additional 120 bytes 
> for each subsequent cached class (per target capturing class) with 
> occasional "jumps" as CHM is resized...
>
> Comparing that with 504 bytes which is the size of j.l.Class object 
> (admittedly this also measures non-java fields, but excludes deep 
> non-java structures) of a freshly loaded class, we can get a feeling 
> of the effectivity of caching. This is only Java-side of things. I 
> don't know what's the additional overhead of each class in the JVM 
> though. But I can imagine it's not zero.
>
> Additional area I'm still experimenting with is the caching strategy:
> - caching is not needed for non-serializable proxy classes that call 
> synthetic methods generated by javac for lambda bodies. Those are 
> already guaranteed to contain only one capture site per target method.
> - caching of proxy classes for method references could be performed on 
> the class that hosts the target method not on the capturing class. 
> This could be much more efficient (think of frequently used shortcuts 
> like Math::min, etc...). If all types appearing in LambdaMetaFactory 
> arguments are also visible from the ClassLoader of the target method's 
> declaring class, this could be done, otherwise caching can fall-back 
> to the capturing class.
>
> I will folow-up with my findings (and some code ;-).
>



More information about the lambda-dev mailing list