Upgrade to Pro — share decks privately, control downloads, hide ads and more …

InvokeDynamic in Practice with JRuby

headius
February 01, 2025

InvokeDynamic in Practice with JRuby

A tour of invokedynamic use in JRuby, with challenges and future plans.

Delivered at FOSDEM 2025 in the Free Java Room on February 1, 2025.

headius

February 01, 2025
Tweet

More Decks by headius

Other Decks in Programming

Transcript

  1. Hello FOSDEM! • Charles Oliver Nutter • @headius(@mastodon.social) • [email protected]

    • JRuby developer for 20 years • Java developer for nearly 30 years • Funding JRuby and other OSS with commercial support contracts • Stickers and business cards
  2. My Role Today • JRuby lead • Keep project moving

    and improving with latest JDK features • Ensure community is healthy and growing • Headius Enterprises founder • Provide commercial support to users of JRuby + other OSS • Exploring other ways to fund JRuby + other OSS
  3. JRuby • Ruby on the JVM • Primary focus on

    Ruby compatibility and experience • Standard Ruby libs, frameworks, work fl ow all are the same • JVM for Ruby • Bring the best of the JVM to the Ruby world • Literally every single OpenJDK project is useful to us
  4. "What About Truf fl eRuby?" • JRuby is intended to

    run on any JVM, anywhere • Only JVM features, modulo some native call-outs (POSIX etc) • Deployable on existing servers, desktops, Android • JRuby supports Windows (and a dozen other esoteric platforms) • Truf fl eRuby is limited to GraalVM on Mac, Linux • Depends on native extensions, heavy down/upcalls hurt perf • Truf fl e startup/warmup are even worse than JRuby
  5. MethodHandles • FOSDEM 2018: MethodHandles Everywhere • Introduction to MethodHandles

    • Introduction to InvokeBinder ( fl uent API for MethodHandles) • Implementing a simple language with MethodHandles • https://archive.fosdem.org/2018/schedule/event/method_handles/
  6. JRuby and InvokeDynamic • InvokeDynamic makes JRuby possible • Ruby

    is heavily dynamic, in sometimes surprising ways • JRuby and Indy have evolved together • Still need to support non-Indy mode • Startup, warmup, memory impact can be large • Odd platforms have issues e.g. Android, OpenJ9
  7. Method Calls • Obvious case and earliest use in JRuby

    • Not as straightforward as it seems • Different targets: Ruby, Java, native, ... • Validation and binding: Mutable types in Ruby, overloads in Java • Adapting argument layouts (optionals, varargs, keywords) • Indy allows all of these adaptations to inline!
  8. Calls From Ruby Receiver Invalidation Notes Ruby single arity Type

    identity (passive) Type/method table modi fi cation (active) Direct binding, no boxing, inlines well Ruby variable arity Ruby with keyword args Type identity (passive) Type/method table modi fi cation (active) Direct binding, fully boxed, usually does not EA. May split entry point in future. Core JRuby method (Java) Type identity (passive) Type/method table modi fi cation (active) Matched arity binds directly. Mismatched arity through varargs box. Normal Java/JVM method Same as above, but also mismatched argument types and Java overloads Single-arity matched args is mostly direct. Overloads use varargs. Native downcall Like Ruby to Ruby; overloads are separate methods. JNR: indy all the way to JNI call. Panama: indy all the way. Ruby special calls (super, re fi ned) Partial hierarchy check (super). Local scope method table check. Fully unoptimized currently. Calls with blocks (lambdas) Same as above; block can replace trailing interface arg like lambda. Indirect block dispatch, poor inlining as with lambda.
  9. at java.base/java.lang.Thread.dumpStack(Thread.java:2210) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at org.jruby.dist/org.jruby.javasupport.JavaMethod.invokeDirectWithExceptionHandling(JavaMethod.java:290) at org.jruby.dist/org.jruby.javasupport.JavaMethod.invokeStaticDirect(JavaMethod.java:221)

    at org.jruby.dist/org.jruby.java.invokers.StaticMethodInvoker.call(StaticMethodInvoker.java:23) at org.jruby.dist/org.jruby.java.invokers.StaticMethodInvoker.call(StaticMethodInvoker.java:85) at org.jruby.dist/org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:90) at org.jruby.dist/org.jruby.ir.instructions.CallBase.interpret(CallBase.java:556) at org.jruby.dist/org.jruby.ir.interpreter.InterpreterEngine.processCall(InterpreterEngine.java:372) at org.jruby.dist/org.jruby.ir.interpreter.StartupInterpreterEngine.interpret(StartupInterpreterEngine.java:66) at org.jruby.dist/org.jruby.ir.interpreter.Interpreter.interpretFrameScope(Interpreter.java:174) at org.jruby.dist/org.jruby.ir.interpreter.Interpreter.INTERPRET_METHOD(Interpreter.java:145) at org.jruby.dist/org.jruby.internal.runtime.methods.InterpretedIRMethod.call(InterpretedIRMethod.java:130) at org.jruby.dist/org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:193) at org.jruby.dist/org.jruby.ir.interpreter.InterpreterEngine.processCall(InterpreterEngine.java:352) at org.jruby.dist/org.jruby.ir.interpreter.StartupInterpreterEngine.interpret(StartupInterpreterEngine.java:66) at org.jruby.dist/org.jruby.ir.interpreter.Interpreter.interpretFrameScope(Interpreter.java:174) at org.jruby.dist/org.jruby.ir.interpreter.Interpreter.INTERPRET_METHOD(Interpreter.java:145) at org.jruby.dist/org.jruby.internal.runtime.methods.InterpretedIRMethod.call(InterpretedIRMethod.java:130) at org.jruby.dist/org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:193) at org.jruby.dist/org.jruby.ir.interpreter.InterpreterEngine.processCall(InterpreterEngine.java:352) at org.jruby.dist/org.jruby.ir.interpreter.StartupInterpreterEngine.interpret(StartupInterpreterEngine.java:66) at org.jruby.dist/org.jruby.ir.interpreter.Interpreter.INTERPRET_BLOCK(Interpreter.java:120)
  10. at java.base/java.lang.Thread.dumpStack(Thread.java:2210) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at org.jruby.dist/org.jruby.javasupport.JavaMethod.invokeDirectWithExceptionHandling(JavaMethod.java:290) at org.jruby.dist/org.jruby.javasupport.JavaMethod.invokeStaticDirect(JavaMethod.java:221)

    at org.jruby.dist/org.jruby.java.invokers.StaticMethodInvoker.call(StaticMethodInvoker.java:23) at org.jruby.dist/org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:75) at blah.invokeOther5:dumpStack(blah.rb:8) at blah .️ ❤ def bar #2(blah.rb:8) at org.jruby.dist/org.jruby.internal.runtime.methods.CompiledIRMethod.call(CompiledIRMethod.java:139) at org.jruby.dist/org.jruby.internal.runtime.methods.CompiledIRMethod.call(CompiledIRMethod.java:212) at org.jruby.dist/org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:193) at org.jruby.dist/org.jruby.runtime.callsite.CachingCallSite.fcall(CachingCallSite.java:199) at blah.invokeOther1:bar(blah.rb:4) at blah .️ ❤ def foo #1(blah.rb:4) at org.jruby.dist/org.jruby.internal.runtime.methods.CompiledIRMethod.call(CompiledIRMethod.java:215) at org.jruby.dist/org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:193) at org.jruby.dist/org.jruby.runtime.callsite.CachingCallSite.fcall(CachingCallSite.java:199) at blah.invokeOther0:foo(blah.rb:11) at blah .️ ❤ {} \=\^main\_ #0(blah.rb:11)
  11. def foo bar end def bar java.lang.Thread.dumpStack end 2.times {

    foo } at java.base/java.lang.Thread.dumpStack(Thread.java:2210) at blah .️ ❤ def bar #2(blah.rb:6) at blah .️ ❤ def foo #1(blah.rb:2)
  12. at blah .️ ❤ def bar #2(blah.rb:6) at java.lang.invoke.DirectMethodHandle$Holder.invokeStatic(java.base@21/DirectMethodHandle$Holder) at

    java.lang.invoke.LambdaForm$MH/0x00000070015ab000.invoke(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015abc00.reinvoke(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015ac000.guard(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015abc00.reinvoke(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015ac000.guard(java.base@21/LambdaForm$MH) at java.lang.invoke.Invokers$Holder.linkToCallSite(java.base@21/Invokers$Holder) at blah .️ ❤ def foo #1(blah.rb:2) at java.lang.invoke.DirectMethodHandle$Holder.invokeStatic(java.base@21/DirectMethodHandle$Holder) at java.lang.invoke.LambdaForm$MH/0x00000070015ab000.invoke(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015abc00.reinvoke(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015ac000.guard(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015abc00.reinvoke(java.base@21/LambdaForm$MH) at java.lang.invoke.LambdaForm$MH/0x00000070015ac000.guard(java.base@21/LambdaForm$MH) at java.lang.invoke.Invokers$Holder.linkToCallSite(java.base@21/Invokers$Holder) at blah .️ ❤ {} \=\^main\_ #0(blah.rb:9)
  13. def foo bar(1) end def bar(*args) java.lang.Thread.dumpStack end 2.times {

    foo } at java.base/java.lang.Thread.dumpStack(Thread.java:2210) at blah .️ ❤ def bar #2(blah.rb:6) at blah .️ ❤ def foo #1(blah.rb:2)
  14. Block Call from Java at java.base/java.lang.Thread.dumpStack(Thread.java:2210) at blah .️ ❤

    def bar #3(blah.rb:6) at blah .️ ❤ def foo #2(blah.rb:2) at blah .️ ❤ {} \=\^main\_ #1(blah.rb:9) at org.jruby.dist/org.jruby.runtime.CompiledIRBlockBody.yieldDirect(CompiledIRBlockBody.java:151) at org.jruby.dist/org.jruby.runtime.IRBlockBody.yieldSpecific(IRBlockBody.java:74) at org.jruby.dist/org.jruby.runtime.Block.yieldSpecific(Block.java:160) at org.jruby.dist/org.jruby.RubyFixnum.times(RubyFixnum.java:333) at blah .️ ❤ script(blah.rb:9)
  15. Varargs Ruby to Java def foo bar end def bar(*args)

    java.lang.Thread.dumpStack(*args) end at java.base/java.lang.Thread.dumpStack(Thread.java:2210) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at org.jruby.dist/org.jruby.javasupport.JavaMethod.invokeDirectWithExceptionHandling(JavaMethod.java:290) at org.jruby.dist/org.jruby.javasupport.JavaMethod.invokeStaticDirect(JavaMethod.java:221) at org.jruby.dist/org.jruby.java.invokers.StaticMethodInvoker.call(StaticMethodInvoker.java:23) at blah .️ ❤ def bar #2(blah.rb:10) at blah .️ ❤ def foo #1(blah.rb:6)
  16. Unexplored Call Forms • Dynamic calls from Java? • Need

    a good pattern for rewriting code to indy • Splitting and specialization for blocks (lambdas) • Same problem as in Java, but pervasive • Splitting heuristics are an open research problem • Numeric unboxing • Dif fi cult to do without being too aggressive, generating too much code
  17. Instance Variables class Person def initialize(name, number) @name, @number =

    name, number end attr_accessor :name, :number def add_tag(tag, value) instance_variable_set(:"@#{tag}", value) end end
  18. Instance Variables • Dynamically-allocated object fi elds • Scan method

    table for accesses on fi rst instantiation • Load or generate class "shape" to map ivars to fi elds • Object array for spilled or dynamic variables • InvokeDynamic wires access directly to fi eld or array
  19. Constants and Globals DEBUG = true $debug = true module

    Foo module Bar class Baz end end end Foo::Bar::Baz.new
  20. Constants and Globals • Constants are scoped, assigned lazily, rarely

    overwritten • Globals are global, either constant or frequently modi fi ed • Indy call sites can constantize them effectively • Mutable call site folds value just fi ne • Invalidate globally based on constant or global name • Complex lookup forms like Foo::Bar::Baz could be improved
  21. Ruby Literals • Numeric literal objects constructed lazily • Numeric

    Fixnum (long), Bignum (BigInteger), Float (double) • Ruby Strings/Regexp (byte[] + Encoding) assembled, interned, cached • All-literal Arrays ([1, 2, 3]) and Range (1..10) • To-do: All-literal Hash, Range, Complex, Rational
  22. INVOKEDYNAMIC fixnum(Lorg/jruby/runtime/ThreadContext;)... org/jruby/ir/targets/indy/FixnumObjectSite.bootstrap(... // arguments: 1L INVOKEDYNAMIC frozen(Lorg/jruby/runtime/ThreadContext;)... org/jruby/ir/targets/indy/StringBootstrap.fstring(... //

    arguments: "hello", "UTF-8", 16, "blah.rb", 3 INVOKEDYNAMIC regexp(Lorg/jruby/runtime/ThreadContext;) org/jruby/ir/targets/indy/RegexpObjectSite.bootstrap(... // arguments: "foo", "UTF-8", 512
  23. String Interpolation • Embed constant bits + speci fi cation

    into indy site • Indy chooses from several forms to interpret speci fi cation • N overloads that stitch it together (up to 4 args) • Static bits from site, dynamic from args[] • Simple loop over static + dynamic args[] • Bail out for extremely large forms (>50 elements)
  24. INVOKEDYNAMIC buildDynamicString(..., IRubyObject)Lorg/jruby/RubyString; org/jruby/ir/targets/indy/BuildDynamicStringSite.buildDString(... // arguments: "the value ", //

    string "UTF-8", // encoding 16, // code range " was passed", // string "UTF-8", // encoding 16, // code range 55, // size estimate "UTF-8", // final encoding 0, // frozen? 0, // chilled? 5L, // specification (bit indicates a static piece) 3 // how many bits are relevant ] def foo(a) puts "the value #{a} was passed" end
  25. Runtime Plumbing • Initial lambda construction, captured state construction •

    Heap-based local variables • Direct walk to appropriate scope depth, retrieve/set var • Thread interrupt with safepoints, but invalidates on interrupt 🤔 • Other "constant dynamic" runtime utils used within JRuby
  26. MethodHandles as IR • Object construction: Class.allocate + obj.initialize •

    Negated comparison • != is not(==), !~ is not(=~) • Common forms: Kernel#loop, Integer#times • Anything could be compiled to IR (see my FOSDEM 2018 talk) • But impossible to reconstruct/simulate call stack
  27. class Dumper def initialize java.lang.Thread.dumpStack end end def foo Dumper.new

    end at java.base/java.lang.Thread.dumpStack(Thread.java:2210) at blah .️ ❤ def initialize #2(blah.rb:5) at blah .️ ❤ def foo #3(blah.rb:10)
  28. Stacktrace with LambdaForm • LambdaForm messes up thread dumps, pro

    fi ling • LF frames included in pro fi les • LF adaptations look like separate call paths for same call • The more we use Indy, the harder it is to get accurate pro fi les
  29. java.lang.invoke Objects • Many MethodHandles, LambdaForms in memory • MH

    chains take longer to warm up than trivial inline cache • Dif fi cult to cache anything between runs
  30. red/black no indy bytes objs name 296792 5291 java.lang.invoke.MemberName 85320

    2650 java.lang.invoke.BoundMethodHandle$Species_L 59016 1828 java.lang.invoke.LambdaForm$Name 46472 1436 java.lang.invoke.DirectMethodHandle 649192 16217 java.lang.invoke.MethodType 519464 16217 java.lang.invoke.MethodType$ConcurrentWeakInternSet$WeakEntry 41952 1036 java.lang.invoke.BoundMethodHandle$Species_LL 41800 862 java.lang.invoke.LambdaForm$Name[] 21720 442 java.lang.invoke.LambdaForm 14312 345 java.lang.invoke.DirectMethodHandle$Accessor 14384 248 java.lang.invoke.MethodTypeForm 11904 474 java.lang.invoke.LambdaForm$NamedFunction 11880 237 java.lang.invoke.MethodHandleImpl$CountingWrapper Total: 1.814MB
  31. red/black with indy bytes objs name 92040 1907 java.lang.invoke.MethodHandleImpl$CountingWrapper 158712

    3955 java.lang.invoke.BoundMethodHandle$Species_LL 88008 2734 java.lang.invoke.DirectMethodHandle 14344 3557 java.lang.invoke.LambdaForm$Name 81768 1693 java.lang.invoke.BoundMethodHandle$Species_L4 181160 5645 java.lang.invoke.BoundMethodHandle$Species_L 1333512 33325 java.lang.invoke.MethodType 1066920 33325 java.lang.invoke.MethodType$ConcurrentWeakInternSet$WeakEntry 62608 1350 java.lang.invoke.LambdaForm$Name[] 31320 642 java.lang.invoke.LambdaForm 614312 10961 java.lang.invoke.LambdaFormEditor$Transform 15840 638 java.lang.invoke.LambdaForm$NamedFunction 15232 368 java.lang.invoke.DirectMethodHandle$Accessor 15616 270 java.lang.invoke.MethodTypeForm 48912 1210 java.lang.invoke.BoundMethodHandle$Species_L3 6976 140 java.lang.invoke.MethodHandle[] 9048 178 java.lang.invoke.BoundMethodHandle$Species_L5 4512 100 java.lang.invoke.DirectMethodHandle$Constructor 4248 155 java.lang.invoke.SwitchPoint Total: 4.278MB
  32. No Indy from Java • Really need a way to

    inject invokedynamic in Java code • Static CallSite.invoke gets us close, but dat Throwable 😵 • Build-time rewriting as a fi rst step? • Give up on Java and move all to Ruby?
  33. JRuby + Indy Future • JRuby 10 will fi ll

    in many gaps • Better adaptations for argument forms, overloads • Finally wire up Panama for native • Explore more intrinsic forms and specialization • Looking forward to working with OpenJDK projects more
  34. Thank You! • Charles Oliver Nutter • [email protected] • @headius(@mastodon.social)

    • blog.headius.com • github.com/sponsors/headius • headius.com/support • github.com/jruby/jruby • github.com/jruby/jcodings • github.com/jruby/joni • github.com/jnr