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