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

Exit(ing) through the YJIT

Exit(ing) through the YJIT

When optimizing code for the YJIT compiler it can be difficult to figure out what code is exiting and why. While working on tracing exits in a Ruby codebase, I found myself wishing we had a tool to reveal the exact line that was causing exits to occur. We set to work on building that functionality into Ruby and now we are able to see every side-exit and why. In this talk we’ll learn about side-exits and how we built a tracer for them. We’ll explore the original implementation, how we rewrote it in Rust, and lastly why it’s so important to always ask "can I make what I built even better?"

Eileen M. Uchitelle

December 01, 2022
Tweet

More Decks by Eileen M. Uchitelle

Other Decks in Programming

Transcript

  1. EXIT(ing) Through the YJIT

  2. None
  3. Eileen M. Uchitelle Twitter / GitHub: @eileencodes Mastadon: @[email protected]

  4. None
  5. None
  6. EXIT(ing) Through the YJIT

  7. What is a JIT?

  8. What is a JIT? "Just in time" compiler

  9. MJIT "Method-Based" JIT

  10. YJIT "Yet Another" JIT

  11. None
  12. def call_method(arg) if arg == "hello" puts arg else puts

    "bye" end end YJIT only compiles code that is run
  13. def call_method(arg) if arg == "hello" puts arg else puts

    "bye" end end call_method("hello") YJIT only compiles code that is run
  14. def call_method(arg) if arg == "hello" puts arg else puts

    "bye" end end call_method("hello") YJIT only compiles code that is run
  15. def call_method able_to_compile not_able_to_compile end call_method YJIT can compile parts

    of methods
  16. def call_method able_to_compile not_able_to_compile end call_method YJIT can compile parts

    of methods
  17. def call_method able_to_compile not_able_to_compile end call_method YJIT can compile parts

    of methods
  18. How is YJIT implemented?

  19. MJIT vs YJIT Which to use?

  20. How can I use YJIT?

  21. $ docker pull rubylang/ruby:master-debug- nightly-focal Docker Images

  22. Pre-requisites • autoconf • make • bison • GCC or

    Clang • libyaml • [email protected] • Rust (for YJIT) • Ruby
  23. Run autogen $ cd ruby $ ./autogen.sh

  24. Configure Ruby $ ./configure --prefix=~/.rubies/ruby-yjit --with-openssl-dir=$(brew --prefix [email protected]) --disable-install-doc

  25. Configure Ruby: Set prefix $ ./configure --prefix=~/.rubies/ruby-yjit --with-openssl-dir=$(brew --prefix [email protected])

    --disable-install-doc
 

  26. Configure Ruby: openssl $ ./configure --prefix=~/.rubies/ruby-yjit --with-openssl-dir=$(brew --prefix [email protected]) --disable-install-doc

  27. Configure Ruby: Disable rdoc $ ./configure --prefix=~/.rubies/ruby-yjit --with-openssl-dir=$(brew --prefix [email protected])

    --disable-install-rdoc
 

  28. Install Ruby $ make install

  29. $ RUBY_YJIT_ENABLE=1 ruby my_script.rb $ ruby --yjit my_script.rb $ ruby

    --jit my_script.rb Enabling YJIT
  30. YJIT dev mode $ ./configure --enable-yjit=dev --prefix=~/.rubies/ruby-yjit --with-openssl-dir=$(brew --prefix [email protected])

    $ make install
  31. "Exit" When YJIT is unable to compile

  32. ar_demo.rb ActiveRecord::Base.establish_connection( adapter: "sqlite3", database: ":memory:") ActiveRecord::Schema.define do create_table :posts,

    force: true do |t| end end class Post < ActiveRecord::Base end class BugTest < ActiveSupport::TestCase def test_create Post.create! end end
  33. Running with stats enabled $ bundle exec ruby --yjit-stats ar_demo.rb

  34. None
  35. None
  36. None
  37. None
  38. But what does the code look like?

  39. "We can totally make that work." - Aaron Patterson

  40. Tracing YJIT Exits

  41. None
  42. Tracing exits $ bundle exec ruby --yjit-trace-exits ar_demo.rb

  43. None
  44. None
  45. None
  46. None
  47. ar_demo.rb [...] class Post < ActiveRecord::Base def call_method(options) one, two,

    three = options end end class BugTest < ActiveSupport::TestCase def test_create post = Post.create! post.call_method("hello") end end
  48. Fixing broken code $ bundle exec ruby --yjit-trace-exits --yjit-call-threshold=1 ar_demo.rb

  49. None
  50. None
  51. None
  52. How will we use this?

  53. Optimizing YJIT

  54. yjit/src/codegen.rs /// Maps a YARV opcode to a code generation

    function (if supported) fn get_gen_fn(opcode: VALUE) -> Option<InsnGenFn> { let VALUE(opcode) = opcode; let opcode = opcode as ruby_vminsn_type; assert!(opcode < VM_INSTRUCTION_SIZE); match opcode { [...] YARVINSN_send => Some(gen_send), YARVINSN_invokeblock => Some(gen_invokeblock), YARVINSN_invokesuper => Some(gen_invokesuper), YARVINSN_leave => Some(gen_leave), [...]
  55. yjit/src/codegen.rs /// Maps a YARV opcode to a code generation

    function (if supported) fn get_gen_fn(opcode: VALUE) -> Option<InsnGenFn> { let VALUE(opcode) = opcode; let opcode = opcode as ruby_vminsn_type; assert!(opcode < VM_INSTRUCTION_SIZE); match opcode { [...] YARVINSN_send => Some(gen_send), YARVINSN_invokeblock => Some(gen_invokeblock), YARVINSN_invokesuper => Some(gen_invokesuper), YARVINSN_leave => Some(gen_leave), [...]
  56. yjit/src/codegen.rs asm.jne( counted_exit!( ocb, side_exit, invokesuper_block ).into() );

  57. Optimizing our Ruby Code

  58. Try YJIT on your app!

  59. Help us improve YJIT

  60. Eileen M. Uchitelle Twitter / GitHub: @eileencodes Mastadon: @[email protected]