Reducing classes loaded by ClassFile API usage

Claes Redestad claes.redestad at oracle.com
Thu May 2 13:12:28 UTC 2024


I’m curious what data we have on which cases of online transformations are common, and which of those common use cases are most performance-sensitive?

Regardless, I’m just looking for constructive ways to reduce the bootstrap overheads of the API. What we have here today is getting close to being acceptable, but we would be looking at a multitude of regressions if #17108 is integrated..

2 maj 2024 kl. 14:04 skrev Brian Goetz <brian.goetz at oracle.com>:

The benchmark that we used most frequently when writing the library was the null adaptation benchmark, where we visit a class file with a transform that just passes the elements through. It is a measure of the cost to traverse and inflate the representation.

We prioritize the case where a class file is transformed with only small changes because this is one of the most common cases for online transformation.

Sent from my iPad

On May 2, 2024, at 7:58 AM, Claes Redestad <claes.redestad at oracle.com> wrote:

 A performance loss where exactly?

For classfile generation (and reflection) I’d be more concerned with cold-to-lukewarm cases of getting an app up and running than, say, the number you might get in synthetic benchmarks running the API at peak performance.


2 maj 2024 kl. 13:50 skrev Brian Goetz <brian.goetz at oracle.com>:

For what it’s worth, there was an earlier experiment that merged the bound and unbound representation classes, and you could see a measurable performance loss just because these classes have more fields and there were more branches to access them.

Sent from my iPad

On May 2, 2024, at 7:34 AM, Claes Redestad <claes.redestad at oracle.com> wrote:

 Hi,

Looking at replacing ASM with the ClassFile API (CFA) in various places we have observed both startup and footprint regressions. Startup times increase 4-5 ms on Hello World, 40 ms on a small GUI app and 250ms on a larger app. So there’s both a one-off cost and a scaling factor here.

We’ve been doing some analysis and picked a lot of low-hanging fruit. Bytecode executed has been reduced to about the same level and we’ve found improvements in dependencies such as the java.lang.constant API. All good. And the number of classes loaded on a Hello World style application has dropped by about 50. Great!

Still the overall picture persists: a Hello World style application that initializes a lambda takes a wee bit longer and the footprint is decidedly. The main culprit now that some low-hanging fruit has been plucked seem to be that the trivial use of CFA to spin up lambda proxies is loading in about 160 classes: An ASM-based baseline loads 691 classes. The best recent CFA version (a merge of https://github.com/openjdk/jdk/pull/19006 and https://github.com/openjdk/jdk/pull/17108) loads 834. A net 143 class difference.

Loading classes slows down startup, increases memory footprint, grows the default CDS archive. And involving more classes - and more code - is often costly even accompanied with some of the solutions being explored to ”fix” startup at large.

So why is this?

The CFA is mainly split up into two package stuctures, one public under java.lang.classfile and one internal under jdk.internal.classfile.impl. In the public side most of the types are sealed interfaces, which are then implemented by an assortment of abstract and concrete classes under jdk.internal.classfile.impl. Very neat. But I do fear this means we are at least doubling the number of loaded classes from this neat separation.

While it’s a bit late in the game I still feel I must propose striking up a conversation about what, if anything, we could consider that would reduce the number of loaded classes. Whether they are interfaces, abstract or concrete classes. I think any savings would be very welcome.

Here’s an idea:

There are a number of cases where the separation seem unnecessary:

public sealed interface ArrayLoadInstruction extends Instruction
        permits AbstractInstruction.UnboundArrayLoadInstruction {
…

    static ArrayLoadInstruction of(Opcode op) {
        Util.checkKind(op, Opcode.Kind.ARRAY_LOAD);
        return new AbstractInstruction.UnboundArrayLoadInstruction(op);
    }
}

An interface in java.lang.classfile.instruction which only permits a single implementation class -  and as it happens has a static factory method which is the only place where that concrete instruction is called.

Making single-use interfaces such as this one a final class is doable[1], but now we’d have some instructions modeled as an interface, others as classes. Cats and dogs, living together. And it gets messy quick for instructions that can be bound or unbound, since those inherit from abstract BoundInstruction or UnboundInstruction respectively. But perhaps internal implementation details like whether an instruction is bound or unbound ought to be modeled with composition rather than inheritance (and optional CodeImpl + pos tuple) in a shared base class? Then it might follow that each of the interfaces in java.lang.classfile.instruction can really be a single final class. If all concrete instructions were folded into their corresponding interface that could reduce the total number of implementation classes by 46 (though only 6 of those seem to be on a Hello World)

Yikes, that’s a deep cut for a small, incremental gain. From an API consumer point of view I can’t say there’s much difference, and the factories can still (be evolved to) produce different concrete types when necessary.

Maybe someone can think of other, simpler ways to reduce the number of types floating around in the ClassFile API?

Thank you for your consideration.

Claes

[1] https://github.com/openjdk/jdk/compare/master...cl4es:jdk:fold_instruction_example?expand=1


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/classfile-api-dev/attachments/20240502/e7d05eea/attachment-0001.htm>


More information about the classfile-api-dev mailing list