-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Notable part of caffeine.jar is consumed by generated classes #110
Comments
The intent is of course to reduce the in-memory overhead due to the many configuration options at the cost of larger disk footprint (unloaded classes). I think the large enum used by
I tried to use inheritance to reduce duplicate code. I think Node was optimal and LocalCache was close, but not perfect. Perhaps there are some more tricks to optimize reuse, e.g. interfaces with default methods. We could also remove the modifiers, as perhaps the JVM is creating unnecessary bridge methods. The biggest win would be to review |
This patch expands on the suggestion in ben-manes#110 (comment) Instead of the proposed numeric code the name of factory is used as the classname to instantiate using reflection. This reduces the class size with 21K bringing the jar size to 786K.
This patch expands on the suggestion in ben-manes#110 (comment) Instead of the proposed numeric code the name of factory is used as the classname to instantiate using reflection. This reduces the class size with 21K bringing the jar size to 786K.
I could imagine, that keeping the small factories small makes sense, but for example, But such optimizations are ugly. For it, to be efficient, you may have to reorder the hierarchy so that the heaviest additions come first. Let's say, you deal with The size optimizations for nodes are clearly much more important and in the long run, you'll probably need something smart as the code size doubles with every new feature. A funny optimization would be to declare |
For variable expiration, I decided to avoid the extra codegen and encoded it onto the existing timestamp fields. Since it cannot be used with fixed expiration ( The enum in The unit tests run against every combination allowed by the |
As advertised, the overhead of method handles is near noise compared to a direct call. So I think we can switch to that for the enum and any other reflective calls. The difference is that a direct call has a much more stable throughput, whereas the handle varies more per iteration making the JIT sensitive. Since the difference is 1-2ns and only on an insertion, its a nice win.
The subset the code of interest is, static final class MethodHandleFactory {
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private static final MethodType METHOD_TYPE = MethodType.methodType(void.class, int.class);
private final MethodHandle methodHandle;
MethodHandleFactory() {
try {
methodHandle = LOOKUP.findConstructor(Alpha.class, METHOD_TYPE);
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
Alpha invoke(int x) {
try {
return (Alpha) methodHandle.invoke(x);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
Alpha invokeExact(int x) {
try {
return (Alpha) methodHandle.invokeExact(x);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
} |
Have a look at #199 |
Okay, let me take a look to understand what you did. :) I was playing around with removing the enum in NodeFactory with method handles. I had a partial prototype and am fine dropping it. Please also take a look to see if any of that is useful with your technique. |
I'm sure @ben-manes checked this too but I thought it would be worth adding a comment. I checked out master, built the jar locally and used javap -l to verify debug symbols are still present in the core classes. |
Last effort: #200 If we use non-nested classes that's extra 49KB in savings (shorter FQN in the classfiles). |
Thanks @jvassev! We're at 648K now. Is everyone satisfied or do we want to leave this open for future iterations? |
I think at this point only a minifier/obfuscator can help. The only low-hanging fruit left is to put the generated classes in a package with a shorter name but I wouldn't like that (and it may not play well with java modules). If runtime code-gen is used this the total would be around ~400K (250core + 90asm + 50generator) but its a whole new territory. So probably this can be closed. |
Yeah, runtime codegen might be a bit too much magic. Its nice to know a lower bound, so thanks for that insight. The only idea I'd have is to review the LocalCache subtypes for which pull their weight. For example, is special casing |
Also, a note to myself, We should add a little JavaDoc to each generated class with the decoded configuration. Then debugging with the source jar (e.g. IDE) will be easier. |
Added the JavaDoc so that anyone reading the types don't need to decode the names, but it states the generated and inherited features. Should make it a tiny bit easier for a user to debug. Closing, thanks all the help. |
Regarding your comment about I see |
Yes, the cache might have been overkill. I wrote the generator and iterated as features were implemented. There are a lot of fields only used for particular configurations, which could be null. We could drop that codegen at the small cost of many unused fields, which is probably okay. |
When I removed the nodeFactory classes (making a node a factory for self) there were 90K in savings. |
I think its worth giving it a try, if you are up for it. It would be a worthwhile experiment worst case. In this case I think we'd end up removing all of the |
Released. Thanks @jvassev! |
caffeine-2.3.1.jar -- 968137 bytes
caffeine-2.3.1.jar without LocalCacheFactory$... and NodeFactory$... classes -- 248414 bytes
Are there any ideas/plans to reduce that?
The text was updated successfully, but these errors were encountered: