Project

General

Profile

Actions

Bug #16941

closed

MJIT doesn't identify Struct kind of instruction

Added by abhsha (Abhishek Sharma) almost 4 years ago. Updated almost 4 years ago.

Status:
Closed
Target version:
-
ruby -v:
2.6.0p0 (2018-12-25 revision 66547) [x86_64-linux]
[ruby-core:98689]

Description

MJIT does not identify Struct instructions and gives a warning while compiling to C.

Example:
def my_method
1.times do
a_struct = Struct.new(:a).new
a_struct.a = "a"
end
end

my_method

$: ruby --jit-save-temps --jit-min-calls=1 --disable-gems --jit --jit-verbose=2 --jit-wait test.rb

output:
start compilation: a=@test.rb:3 -> /tmp/_ruby_mjit_p30696u2.c
MJIT warning: Skipped to compile unsupported instruction: opt_call_c_function
JIT failure (0.0ms): a=@test.rb:3 -> /tmp/_ruby_mjit_p30696u2.c

Are structs not recognised by JIT compiler?
We have this call in our programs many number of times (~ 10^6), it attempts every time and fails. Can this be a cause of programs to get slower with --jit enabled ?

Actions #1

Updated by abhsha (Abhishek Sharma) almost 4 years ago

  • ruby -v changed from 2.6.0 to 2.6.0p0 (2018-12-25 revision 66547) [x86_64-linux]

Updated by shyouhei (Shyouhei Urabe) almost 4 years ago

  • Assignee set to k0kubun (Takashi Kokubun)

Updated by k0kubun (Takashi Kokubun) almost 4 years ago

  • Status changed from Open to Feedback

Are structs not recognised by JIT compiler?

To be precise, opt_call_c_function insn is not supported by JIT compiler, and Struct is one of a few things which may generate opt_call_c_function insn. It's something to be fixed for sure, but as of now priority of fixing it is low.

We have this call in our programs many number of times (~ 106), it attempts every time and fails. Can this be a cause of programs to get slower with --jit enabled ?

Well, if it attempts of JIT compilation every time, it'd be compiled every time even if JIT supported opt_call_c_function. And yes, it (not the failure, but running JIT compilation many times) could be the reason of the slowness.

The fundamental problem in the script you provided is that it's defining a brand-new class by Struct.new every time my_method is called. Note that it's not good for VM's performance either. You should define a Struct class outside my_method and my_method should just refer to it. If it's not possible in your use case, there's nothing we can do.

One thing to note is that --jit-min-calls is 5 in Ruby 2.6's default, and it's 10000 in Ruby 2.7's default. Therefore the problem may not happen in the my_method with Ruby 2.7 or later.

Updated by abhsha (Abhishek Sharma) almost 4 years ago

Thanks, k0kubun for the feedback. I didn't understand the need to compile the block every time it is being called. Can you refer me to some documentation for understanding execution with JIT?

Regarding the Struct, I think yes we can remove it outside of the method.

-Abhishek

Updated by k0kubun (Takashi Kokubun) almost 4 years ago

I didn't understand the need to compile the block every time it is being called.

Good point. If there's only one block definition, only one JIT compilation attempt should happen regardless of how Struct is handled in it. I think I was a little bit confused, and I need more information to understand your problem more.

Could you provide a single script and a command which reproduces "it attempts every time" a method is called? In your current script and logs in the description, my_method is called only once and it's compiled once, and I don't see the "it attempts every time" problem there.

Can you refer me to some documentation for understanding execution with JIT?

The only official documentation of JIT available right now is https://bugs.ruby-lang.org/projects/ruby/wiki/MJIT, but I think your problem seems a little bit complicated. So let me figure out what the problem is first to revise my explanation of it for you.

Updated by abhsha (Abhishek Sharma) almost 4 years ago

Hello K0kubun,

Here is the revised script

def my_method
    a_struct = Struct.new(:a).new
    a_struct.a = "a"
    puts "Inside this Method"
end

3.times{ puts "=="; my_method; puts "==" }

Output with --jit-min-calls=1

ruby --jit-save-temps --disable-gems --jit-min-calls=1 --jit-verbose=2 --jit-wait test.rb
MJIT: CC defaults to /tool/pandora64/bin/gcc
MJIT: tmp_dir is /tmp
Creating precompiled header
Starting process: /tool/pandora64/bin/gcc /tool/pandora64/bin/gcc -w -Wfatal-errors -fPIC -shared -w -pipe -O3 -o /tmp/_ruby_mjit_hp23581u0.h.gch /tool/pandora64/.package/ruby-2.6.0/include/ruby-2.6.0/x86_64-linux/rb_mjit_min_header-2.6.0.h
start compilation: block in <main>@test.rb:7 -> /tmp/_ruby_mjit_p23581u0.c
Starting process: /tool/pandora64/bin/gcc /tool/pandora64/bin/gcc -w -Wfatal-errors -fPIC -shared -w -pipe -O3 -o /tmp/_ruby_mjit_p23581u0.o /tmp/_ruby_mjit_p23581u0.c -c -Wl,--compress-debug-sections=zlib -nostartfiles -nodefaultlibs -nostdlib
Starting process: /tool/pandora64/bin/gcc /tool/pandora64/bin/gcc -shared -Wfatal-errors -fPIC -shared -w -pipe -O3 -o /tmp/_ruby_mjit_p23581u0.so /tmp/_ruby_mjit_p23581u0.o -Wl,--compress-debug-sections=zlib -nostartfiles -nodefaultlibs -nostdlib
JIT success (119.4ms): block in <main>@test.rb:7 -> /tmp/_ruby_mjit_p23581u0.c
==
start compilation: my_method@test.rb:1 -> /tmp/_ruby_mjit_p23581u1.c
Starting process: /tool/pandora64/bin/gcc /tool/pandora64/bin/gcc -w -Wfatal-errors -fPIC -shared -w -pipe -O3 -o /tmp/_ruby_mjit_p23581u1.o /tmp/_ruby_mjit_p23581u1.c -c -Wl,--compress-debug-sections=zlib -nostartfiles -nodefaultlibs -nostdlib
Starting process: /tool/pandora64/bin/gcc /tool/pandora64/bin/gcc -shared -Wfatal-errors -fPIC -shared -w -pipe -O3 -o /tmp/_ruby_mjit_p23581u1.so /tmp/_ruby_mjit_p23581u1.o -Wl,--compress-debug-sections=zlib -nostartfiles -nodefaultlibs -nostdlib
JIT success (176.5ms): my_method@test.rb:1 -> /tmp/_ruby_mjit_p23581u1.c
start compilation: a=@test.rb:2 -> /tmp/_ruby_mjit_p23581u2.c
MJIT warning: Skipped to compile unsupported instruction: opt_call_c_function
JIT failure (0.0ms): a=@test.rb:2 -> /tmp/_ruby_mjit_p23581u2.c
Inside this Method
==
==
start compilation: a=@test.rb:2 -> /tmp/_ruby_mjit_p23581u3.c
MJIT warning: Skipped to compile unsupported instruction: opt_call_c_function
JIT failure (0.0ms): a=@test.rb:2 -> /tmp/_ruby_mjit_p23581u3.c
Inside this Method
==
==
start compilation: a=@test.rb:2 -> /tmp/_ruby_mjit_p23581u4.c
MJIT warning: Skipped to compile unsupported instruction: opt_call_c_function
JIT failure (0.0ms): a=@test.rb:2 -> /tmp/_ruby_mjit_p23581u4.c
Inside this Method
==
Stopping worker thread
Successful MJIT finish

From the documentation, I understand on 1st (jit-min-calls) call a method gets queued to be "JITed", and since I'm using --jit-wait, it waits for the method to get JIT-compiled. The only problem here is that it doesn't consider if a process has been attempted to get JITed before and has failed to do so. Therefore it attempts to recompile again and again.

I have a more greedy thought, is it possible to have a mechanism to persist the JITed methods and load on a later point of time? This could be useful for creating fast gems without the need to use the C interface to write code. Also, it can be a useful scenario, where the same code runs for very large number of times. For Example, delivering software to the client, where he is just interested in running the software as a black box and get desired output.

Thanks,
Abhishek

Updated by k0kubun (Takashi Kokubun) almost 4 years ago

Thanks, I figured out the problem from your script.

Every time we call Struct.new(:a), it creates a different method entry :a=. Every time you call Struct.new(:a) and :a=, MJIT finds a new method entry of :a= and it therefore tries to compile it. There's nothing wrong going on in MJIT's side, but the Ruby VM could reuse a method entry which was previously because the method definition is the same for each index, and that (or just stopping to call Struct.new every time) is the only way we could fix the problem you shared.

I suppose this won't be a problem when you use --jit-min-calls=10000 (default of Ruby 2.7 or later). Is it the case, or do you think it should be fixed?

The only problem here is that it doesn't consider if a process has been attempted to get JITed before and has failed to do so. Therefore it attempts to recompile again and again.

As I explained in this comment, this part is not quite right. It has never attempted JIT for the same method entry again. The script is creating a different method every time.

I have a more greedy thought, is it possible to have a mechanism to persist the JITed methods and load on a later point of time?

This is a done discussion [Feature #14489]. It's unfortunately a low priority for the time being. I may revisit it once I finish some more priorities I have.

Updated by abhsha (Abhishek Sharma) almost 4 years ago

Hi k0kubun,
I think it should be Ok for me with --jit-min-calls=10000. Also, I will take the struct declaration outside of the block, so it shouldn't be a problem anymore for my case.

Thanks for all the help. With this ticket I developed an interest in Ruby internals and moving forward, I would like to contribute to the community. I have written a separate email to you to further our discussion as it is beyond the scope of this ticket. :)

Thanks,
Abhishek

Actions #9

Updated by k0kubun (Takashi Kokubun) almost 4 years ago

  • Status changed from Feedback to Closed
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0