$30 off During Our Annual Pro Sale. View Details »

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

    View Slide

  2. View Slide

  3. Eileen M. Uchitelle
    Twitter / GitHub:


    @eileencodes


    Mastadon:


    @[email protected]

    View Slide

  4. View Slide

  5. View Slide

  6. EXIT(ing)


    Through


    the YJIT

    View Slide

  7. What is a JIT?

    View Slide

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

    View Slide

  9. MJIT
    "Method-Based" JIT

    View Slide

  10. YJIT
    "Yet Another" JIT

    View Slide

  11. View Slide

  12. def call_method(arg)


    if arg == "hello"


    puts arg


    else


    puts "bye"


    end


    end


    YJIT only compiles


    code that is run

    View Slide

  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

    View Slide

  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

    View Slide

  15. def call_method


    able_to_compile


    not_able_to_compile


    end


    call_method
    YJIT can compile


    parts of methods

    View Slide

  16. def call_method


    able_to_compile


    not_able_to_compile


    end


    call_method
    YJIT can compile


    parts of methods

    View Slide

  17. def call_method


    able_to_compile


    not_able_to_compile


    end


    call_method
    YJIT can compile


    parts of methods

    View Slide

  18. How is YJIT
    implemented?

    View Slide

  19. MJIT vs YJIT
    Which to use?

    View Slide

  20. How can I use
    YJIT?

    View Slide

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

    View Slide

  22. Pre-requisites
    • autoconf
    • make
    • bison
    • GCC or Clang
    • libyaml
    [email protected]
    • Rust (for YJIT)
    • Ruby

    View Slide

  23. Run autogen
    $ cd ruby
    $ ./autogen.sh

    View Slide

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

    View Slide

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


    View Slide

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

    View Slide

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


    View Slide

  28. Install Ruby
    $ make install

    View Slide

  29. $ RUBY_YJIT_ENABLE=1 ruby my_script.rb
    $ ruby --yjit my_script.rb
    $ ruby --jit my_script.rb
    Enabling YJIT

    View Slide

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

    View Slide

  31. "Exit"
    When YJIT is


    unable to compile

    View Slide

  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

    View Slide

  33. Running with stats enabled
    $ bundle exec ruby --yjit-stats ar_demo.rb

    View Slide

  34. View Slide

  35. View Slide

  36. View Slide

  37. View Slide

  38. But what does
    the code
    look like?

    View Slide

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

    View Slide

  40. Tracing YJIT
    Exits

    View Slide

  41. View Slide

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

    View Slide

  43. View Slide

  44. View Slide

  45. View Slide

  46. View Slide

  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

    View Slide

  48. Fixing broken code
    $ bundle exec ruby --yjit-trace-exits
    --yjit-call-threshold=1
    ar_demo.rb

    View Slide

  49. View Slide

  50. View Slide

  51. View Slide

  52. How will we
    use this?

    View Slide

  53. Optimizing YJIT

    View Slide

  54. yjit/src/codegen.rs
    /// Maps a YARV opcode to a code generation function (if
    supported)


    fn get_gen_fn(opcode: VALUE) -> Option {


    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),


    [...]

    View Slide

  55. yjit/src/codegen.rs
    /// Maps a YARV opcode to a code generation function (if
    supported)


    fn get_gen_fn(opcode: VALUE) -> Option {


    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),


    [...]

    View Slide

  56. yjit/src/codegen.rs
    asm.jne(


    counted_exit!(


    ocb, side_exit, invokesuper_block


    ).into()


    );


    View Slide

  57. Optimizing our
    Ruby Code

    View Slide

  58. Try YJIT
    on your app!

    View Slide

  59. Help us improve
    YJIT

    View Slide

  60. Eileen M. Uchitelle
    Twitter / GitHub:


    @eileencodes


    Mastadon:


    @[email protected]

    View Slide