Skip to content

Optimize ClassNameReader.getClassName via direct ASM API#36814

Open
cookie-meringue wants to merge 1 commit into
spring-projects:mainfrom
cookie-meringue:optimize-classnamereader
Open

Optimize ClassNameReader.getClassName via direct ASM API#36814
cookie-meringue wants to merge 1 commit into
spring-projects:mainfrom
cookie-meringue:optimize-classnamereader

Conversation

@cookie-meringue
Copy link
Copy Markdown
Contributor

Change

ClassNameReader.getClassName now calls ClassReader.getClassName() directly instead of delegating to the visitor-based getClassInfo(r)[0].

Before

public static String getClassName(ClassReader r) {
    return getClassInfo(r)[0];
}

After

// SPRING PATCH BEGIN
public static String getClassName(ClassReader r) {
    return r.getClassName().replace('/', '.');
}
// SPRING PATCH END

Motivation

ClassNameReader.getClassName is a small utility that extracts the FQCN from class bytecode (e.g. byte[]"com.example.MyService").
It sits on the hot path of every CGLIB proxy class definition.

The previous implementation did all of the following:

  • Allocated an ArrayList and a ClassVisitor.
  • Walked the class header, UTF-8 decoding this_class, super_class, and every interface name.
  • Called replace('/', '.') and array.add() on each name.
  • Threw EARLY_EXIT and caught it to break out of the visitor.
  • Built the result array with toArray(new String[0]).

After all that work, the caller used only [0] of the resultsuper_class and the interface names were thrown away.

public static String getClassName(ClassReader r) {
    return getClassInfo(r)[0];
}

Impact

Per call, the new path:

  • Cuts object allocations from ~7 down to 2.
  • Skips UTF-8 decoding of super_class and every interface name.
  • Removes the visitor dispatch and the throw / catch round-trip.

JMH measurements

ClassNameReaderBenchmark (5 forks × 5 warmup × 5 measurement)

Without additional interfaces — proxy = extends ProxyTarget implements Factory

Path Score Memory Allocation
getClassInfo(r)[0] 216.9 ns/op 816 B/op
r.getClassName().replace('/', '.') 22.0 ns/op 432 B/op
Improvement 9.9× faster 1.9× less (-47%)

With 3 additional interfacesCloneable, Comparable, Runnable

Path Score Memory Allocation
getClassInfo(r)[0] 314.5 ns/op 1024 B/op
r.getClassName().replace('/', '.') 21.7 ns/op 432 B/op
Improvement 14.5× faster 2.4× less (-58%)

How to run

./gradlew :spring-core:jmhJar
java -jar spring-core/build/libs/spring-core-<version>-jmh.jar \
    "ClassNameReaderBenchmark" -prof gc

On the current main branch, <version> is 7.1.0-SNAPSHOT.

The new path's time and allocation stay constant regardless of how many interfaces the proxy declares. The legacy path, by contrast, decodes every interface name from the class header and runs the extra replace / add work — so the gap widens as the proxy implements more interfaces.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label May 18, 2026
@cookie-meringue cookie-meringue force-pushed the optimize-classnamereader branch from 0e8a5f1 to d2b655e Compare May 18, 2026 21:14
getClassName now calls ClassReader.getClassName() directly instead
of routing through the visitor-based getClassInfo. Previously, it
allocated a List and a ClassVisitor and decoded super_class and
every interface name only to discard all but the first element.

The method is on the hot path of every CGLIB proxy class definition,
so this change significantly lowers its per-call processing cost.

Signed-off-by: cookie-meringue <daehyeon3351@gmail.com>
@cookie-meringue cookie-meringue force-pushed the optimize-classnamereader branch from d2b655e to 6045ec5 Compare May 18, 2026 21:30
@sbrannen sbrannen added the in: core Issues in core modules (aop, beans, core, context, expression) label May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in: core Issues in core modules (aop, beans, core, context, expression) status: waiting-for-triage An issue we've not yet triaged or decided on

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants