Skip to content

Introduce VarHandle 9 stub#15486

Merged
franz1981 merged 1 commit into
netty:4.2from
franz1981:4.2_varhandle
Jul 18, 2025
Merged

Introduce VarHandle 9 stub#15486
franz1981 merged 1 commit into
netty:4.2from
franz1981:4.2_varhandle

Conversation

@franz1981

Copy link
Copy Markdown
Contributor

Motivation:

Unsafe unavailability prevent some basic JIT optimizations while accessing buffers and reference counters, causing some pretty severe performance regression

Modifications:

Expose a VarHandle's 9 stub to enable Ja +9 to use it, if Unsafe is unavailable

Result:

Fixes #15485

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a compile‐time VarHandle stub for Java 8 and a runtime fallback to use real VarHandle on Java 9+ when Unsafe is unavailable, restoring JIT optimizations for buffer and ref-count accesses.

  • Added a new varhandle‐stub module that defines java.lang.invoke.VarHandle stubs for Java 8 compilation.
  • Updated PlatformDependent to detect and enable VarHandle support and exposed findVarHandleOfIntField.
  • Extended ReferenceCountUpdater and AbstractReferenceCountedByteBuf to use VarHandle as a fallback for managing reference counts.

Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
varhandle-stub/src/main/java/java/lang/invoke/package-info.java New package-info documenting unstable stub API
varhandle-stub/src/main/java/java/lang/invoke/VarHandle.java Stub implementation of VarHandle with core methods
varhandle-stub/pom.xml POM for the stub module
pom.xml Registered the new varhandle-stub module
common/src/main/java/io/netty/util/internal/VarHandleFactory.java Factory to look up real VarHandle via MethodHandles
common/src/main/java/io/netty/util/internal/ReferenceCountUpdater.java Added VarHandle-based fallback paths in ref-count operations
common/src/main/java/io/netty/util/internal/PlatformDependent.java Detects VarHandle support, removes ordering methods, adds finder
common/pom.xml Added provided‐scope dependency on netty-varhandle-stub
buffer/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBuf.java Integrated VarHandle into bytebuf ref-count logic
buffer/pom.xml Added provided‐scope dependency on netty-varhandle-stub
Comments suppressed due to low confidence (2)

common/src/main/java/io/netty/util/internal/PlatformDependent.java:612

  • Consider adding unit tests for findVarHandleOfIntField to verify behavior both when VarHandle is enabled and disabled, ensuring coverage of fallback logic.
    public static VarHandle findVarHandleOfIntField(Class<?> type, String fieldName) {

common/src/main/java/io/netty/util/internal/PlatformDependent.java:612

  • Add a Javadoc comment describing the purpose, parameters, and return value of findVarHandleOfIntField to improve code discoverability and clarity.
    public static VarHandle findVarHandleOfIntField(Class<?> type, String fieldName) {

Comment thread common/src/main/java/io/netty/util/internal/ReferenceCountUpdater.java Outdated
@franz1981

franz1981 commented Jul 14, 2025

Copy link
Copy Markdown
Contributor Author

on JDK 24
Running getByteBatch before, with -pcheckBounds=true -pcheckAccessible=true -pbatchSize=32 -f 1 -pbufferType=HEAP --jvmArgs="-Dio.netty.noUnsafe=true -Dio.netty.varHandle.enabled=false"

Benchmark                                  Score   Error  Units
ByteBufAccessBenchmark.getByteBatch       36.214 ± 0.499  ns/op

with this PR, with -pcheckBounds=true -pcheckAccessible=true -pbatchSize=32 -f 1 -pbufferType=HEAP --jvmArgs="-Dio.netty.noUnsafe=true -Dio.netty.varHandle.enabled=true"

Benchmark                                  Score   Error  Units
ByteBufAccessBenchmark.getByteBatch       12.446 ± 0.078  ns/op

with unsafe working i.e. -pcheckBounds=true -pcheckAccessible=true -pbatchSize=32 -f 1 -pbufferType=HEAP --jvmArgs="-Dio.netty.noUnsafe=false -Dio.netty.varHandle.enabled=false"

Benchmark                                  Score   Error  Units
ByteBufAccessBenchmark.getByteBatch       13.186 ± 0.842  ns/op

which shows that VarHandle allows the ByteBuf's isAccessiblecheck to NOT use volatile (as expected). We could have usedgetOpaquebut I have decided to not change the existingUnsafe`'s semantic (which was to just treat the read as a plain get).

@normanmaurer

Copy link
Copy Markdown
Member

@franz1981 there are some failing tests... PTAL

@franz1981

Copy link
Copy Markdown
Contributor Author

@normanmaurer I've added another commit to handle the byte[] case too.
Will look into it (tomorrow) 🙏

Comment thread common/src/main/java/io/netty/util/internal/PlatformDependent.java Outdated
@franz1981 franz1981 force-pushed the 4.2_varhandle branch 2 times, most recently from 5728f71 to 2fabb11 Compare July 14, 2025 16:52
@franz1981

Copy link
Copy Markdown
Contributor Author

@normanmaurer @chrisvest I see that I could remove the VarHandle in the updater and just use a MethodHande of private field (that's still requires reflective access since is a java 9+ method, see https://docs.oracle.com/javase/9/docs/api/java/lang/invoke/MethodHandles.html#privateLookupIn-java.lang.Class-java.lang.invoke.MethodHandles.Lookup- - but I will double check)

@franz1981 franz1981 marked this pull request as ready for review July 14, 2025 16:56
@franz1981 franz1981 force-pushed the 4.2_varhandle branch 2 times, most recently from 6937238 to c8f73b7 Compare July 15, 2025 03:29
@franz1981

franz1981 commented Jul 15, 2025

Copy link
Copy Markdown
Contributor Author

@normanmaurer I had to remove VarHandle as a return type from the signature of a method, to prevent Java 8 to complain with

java.lang.SecurityException: Prohibited package name: java.lang.invoke

but I'm still getting some error which I cannot reproduce locally e.g.

Error:  io.netty.buffer.UnpooledByteBufAllocatorTest.testBufferWithCapacity -- Time elapsed: 0 s <<< ERROR!
java.lang.invoke.WrongMethodTypeException: cannot convert MethodHandle(VarHandle,AbstractReferenceCountedByteBuf,int)void to (VarHandle,Object[])void
	at java.base/java.lang.invoke.MethodHandle.asTypeUncached(MethodHandle.java:915)
	at java.base/java.lang.invoke.MethodHandle.setAsTypeCache(MethodHandle.java:900)
	at java.base/java.lang.invoke.MethodHandle.asType(MethodHandle.java:873)
	at io.netty.util.internal.ReferenceCountUpdater.setInitialValue(ReferenceCountUpdater.java:74)
	at io.netty.buffer.AbstractReferenceCountedByteBuf.<init>(AbstractReferenceCountedByteBuf.java:61)
	at io.netty.buffer.UnpooledDirectByteBuf.<init>(UnpooledDirectByteBuf.java:56)
	at io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledDirectByteBuf.<init>(UnpooledByteBufAllocator.java:225)
	at io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:95)
	at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:188)
	at io.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:124)
	at io.netty.buffer.ByteBufAllocatorTest.testBufferWithCapacity(ByteBufAllocatorTest.java:54)
	at io.netty.buffer.ByteBufAllocatorTest.testBufferWithCapacity(ByteBufAllocatorTest.java:48)
	at java.base/java.lang.reflect.Method.invoke(Method.java:565)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)

now investigating, since I see that the VarHandle in AbstractReferenceCountedByteBuf is called in a generic method signature as

    public void setInitialValue(T instance) {
        final long offset = unsafeOffset();
        if (offset == -1) {
            Object vho = varHandleUpdater();
            int initialValue = initialValue();
            if (vho != null) {
                VarHandle vh = (VarHandle) vho;
                vh.set(instance, (int) initialValue);  // <-------- THIS IS THE ERROR
                VarHandle.storeStoreFence();
            } else {
                updater().set(instance, initialValue);
            }
        } else {
            PlatformDependent.safeConstructPutInt(instance, offset, initialValue());
        }
    }

which enforce the "right" type at runtime, not at compile time.

At compile time the invocation is using
42 invokevirtual #43 <java/lang/invoke/VarHandle.set : (Lio/netty/util/ReferenceCounted;I)V>

I'll come up with a solution for that 👍

@franz1981

Copy link
Copy Markdown
Contributor Author

There's still something which is surprising: if I write a sample program (using VarHandle) which receiver type is wider at compile time e.g. Object vs what's expected:

  • I'm not getting any failure
  • performance seems ok (and assembly too - verified)

@franz1981

franz1981 commented Jul 15, 2025

Copy link
Copy Markdown
Contributor Author

On the CI I'm still getting hit by the same error on JDK 24-25

java.lang.invoke.WrongMethodTypeException: cannot convert MethodHandle(VarHandle,AbstractReferenceCountedByteBuf,int)void to (VarHandle,Object[])void
	at java.base/java.lang.invoke.MethodHandle.asTypeUncached(MethodHandle.java:915)
	at java.base/java.lang.invoke.MethodHandle.setAsTypeCache(MethodHandle.java:900)
	at java.base/java.lang.invoke.MethodHandle.asType(MethodHandle.java:873)
	at io.netty.buffer.AbstractReferenceCountedByteBuf$1.setVarHandleRefCnt(AbstractReferenceCountedByteBuf.java:51)
	at io.netty.buffer.AbstractReferenceCountedByteBuf$1.setVarHandleRefCnt(AbstractReferenceCountedByteBuf.java:38)
	at io.netty.util.internal.ReferenceCountUpdater.setInitialValue(ReferenceCountUpdater.java:73)
	at io.netty.buffer.AbstractReferenceCountedByteBuf.<init>(AbstractReferenceCountedByteBuf.java:70)
	at io.netty.buffer.UnpooledHeapByteBuf.<init>(UnpooledHeapByteBuf.java:51)
	at io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledHeapByteBuf.<init>(UnpooledByteBufAllocator.java:160)
	at io.netty.buffer.UnpooledByteBufAllocator.newHeapBuffer(UnpooledByteBufAllocator.java:85)
	at io.netty.buffer.AbstractByteBufAllocator.heapBuffer(AbstractByteBufAllocator.java:169)
	at io.netty.buffer.Unpooled.buffer(Unpooled.java:138)
	at io.netty.buffer.BigEndianHeapByteBufTest.newBuffer(BigEndianHeapByteBufTest.java:31)
	at io.netty.buffer.SimpleLeakAwareByteBufTest.newBuffer(SimpleLeakAwareByteBufTest.java:35)
	at io.netty.buffer.AbstractByteBufTest.newBuffer(AbstractByteBufTest.java:91)
	at io.netty.buffer.AbstractByteBufTest.init(AbstractByteBufTest.java:102)
	at io.netty.buffer.SimpleLeakAwareByteBufTest.init(SimpleLeakAwareByteBufTest.java:52)
	at java.base/java.lang.reflect.Method.invoke(Method.java:565)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)

but the signature now is

        @Override
        protected void setVarHandleRefCnt(VarHandle vh, AbstractReferenceCountedByteBuf instance, int refCnt) {
            vh.set(instance, refCnt);
        }

Looking at VarHandle code path (which I'm not an expert of) - there's some form of caching of types which "could" be related the many AbstractReferenceCountedByteBuf subclasses which can call such method - but I have no better guess since I cannot reproduce it ATM

@franz1981

Copy link
Copy Markdown
Contributor Author

It looks like I can finally reproduce it

image

Working on it -> putting this in draft again

@franz1981 franz1981 marked this pull request as draft July 15, 2025 06:11
@derklaro

Copy link
Copy Markdown
Contributor

It could be that the issue comes from the fact that the @PolymorphicSignature annotation is not present on your set method stub, which causes a wrong invokevirtual instruction to be generated (using the actual Object[] type). The javadoc of MethodHandle describes this:

In source code, a call to a signature polymorphic method will compile, regardless of the requested symbolic type descriptor. As usual, the Java compiler emits an invokevirtual instruction with the given symbolic type descriptor against the named method. The unusual part is that the symbolic type descriptor is derived from the actual argument and return types, not from the method declaration.

@franz1981

franz1981 commented Jul 15, 2025

Copy link
Copy Markdown
Contributor Author

Is it worthy to give it a shot @derklaro thanks for the suggestion - although "in theory" the stub shouldn't even be loaded and from the invocation pov (at byte level) it shouldn't matter

@franz1981

franz1981 commented Jul 15, 2025

Copy link
Copy Markdown
Contributor Author

@derklaro and I believe you're right

I just checked a clean example and the bytecode of a proper VarHandle invocation uses strong typed args (and not a Object[]) as the doc you quoted says.
Which means that this is likely the reason of the failure...

Comment thread buffer/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBuf.java Outdated
Comment thread buffer/src/main/java/io/netty/buffer/HeapByteBufUtil.java
@yawkat

yawkat commented Jul 15, 2025

Copy link
Copy Markdown
Contributor

If you go the stubbing route here, maybe it's best to just combine it with the JFR stub instead of having two modules. There might come a point where we want eg a StackWalker stub too.

@franz1981

Copy link
Copy Markdown
Contributor Author

thanks for looking at it @yawkat
I?m stryggling to get this to work ATM due to what @derklaro noticed i.e. the stub has no polymorphic signature and we're not getting it properly typed

I'm checking what I can do about it - unless you know

But I agree that we would like to have a single stubs modules at this point

@derklaro

Copy link
Copy Markdown
Contributor

@franz1981 I quickly skimmed through the jdk source and found that methods annotated with PolymorphicSignature must be native to be picked up as such, see: https://github.com/openjdk/jdk/blob/c9ecc826668575678f11578a67f125d430ebffad/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1330-L1337

Comment thread common/src/main/java/io/netty/util/internal/PlatformDependent.java Outdated
@yawkat

yawkat commented Jul 15, 2025

Copy link
Copy Markdown
Contributor

good catch, making the stub method native makes a simple VH test pass for me

@franz1981 franz1981 marked this pull request as ready for review July 17, 2025 16:50
@franz1981

Copy link
Copy Markdown
Contributor Author

I've added a fix - it was a broken benchmark - now performance is as expected, see dfa88e72a908498f2f940eb7e5137c039fb952f4

@yawkat

yawkat commented Jul 17, 2025

Copy link
Copy Markdown
Contributor

We can leave VarHandle be for NI for now, but the new Configuration-based AIFU initialization has the same problem that NI won't be able to infer the field it belongs to.

@franz1981

franz1981 commented Jul 17, 2025

Copy link
Copy Markdown
Contributor Author

Why? When unsafe or VarHandle won't be available it should be known statically and the updater is allocated without reflection and using constants

Check the latest commit which perform the updater allocation in place

@yawkat

yawkat commented Jul 17, 2025

Copy link
Copy Markdown
Contributor

Ahh you're right, I thought it was a local method call, not a static import.

@normanmaurer

Copy link
Copy Markdown
Member

@franz1981 so this is ready for review ?

@franz1981

Copy link
Copy Markdown
Contributor Author

Yep @normanmaurer now adding one condition and some doc but yep

Comment thread common/src/main/java/io/netty/util/internal/VarHandleFactory.java Outdated

@normanmaurer normanmaurer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally speaking looks good and awesome work. Please add some comments / javadocs to explain why we need this

@franz1981

Copy link
Copy Markdown
Contributor Author

PTAL @normanmaurer added some docs in the varhandle stub to explain why it is how it is and what devs should do while using it

Comment thread common/src/main/java/io/netty/util/internal/ReferenceCountUpdater.java Outdated
Motivation:

Unsafe unavailability prevent some basic JIT optimizations while accessing
buffers and reference counters, causing some pretty severe performance regression

Modifications:

Expose a VarHandle's 9 stub to enable Ja +9 to use it, if Unsafe is unavailable

Result:

Fixes netty#15485
@franz1981

franz1981 commented Jul 18, 2025

Copy link
Copy Markdown
Contributor Author

If it's ready to go it can unblock #15504 ptal @chrisvest or @normanmaurer

@chrisvest chrisvest left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work, @franz1981 👍

@franz1981

Copy link
Copy Markdown
Contributor Author

Thanks @chrisvest 🙏 so, ready to go?

@normanmaurer

Copy link
Copy Markdown
Member

@franz1981 squash and merge when happy with it

@franz1981 franz1981 merged commit daab644 into netty:4.2 Jul 18, 2025
32 of 33 checks passed
@franz1981

Copy link
Copy Markdown
Contributor Author

@normanmaurer remember, right now I have disabled var handle if unsafe is there, since is a more performant option, but for io_uring maybe you still want to use it even if unsafe is available 🙏

@normanmaurer

Copy link
Copy Markdown
Member

@normanmaurer remember, right now I have disabled var handle if unsafe is there, since is a more performant option, but for io_uring maybe you still want to use it even if unsafe is available 🙏

Yep will do once this one is merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unsafe unavailability severely affect Netty 4.2 heap ByteBuf access performance

9 participants