Skip to content

Commit 931bd35

Browse files
k0kubunXrXr
authored andcommitted
YJIT: GC and recompile all code pages (ruby#6406)
when it fails to allocate a new page. Co-authored-by: Alan Wu <alansi.xingwu@shopify.com>
1 parent 2703c32 commit 931bd35

12 files changed

Lines changed: 454 additions & 32 deletions

File tree

cont.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ static VALUE rb_cFiberPool;
6969
#define FIBER_POOL_ALLOCATION_FREE
7070
#endif
7171

72-
#define jit_cont_enabled mjit_enabled // To be used by YJIT later
72+
#define jit_cont_enabled (mjit_enabled || rb_yjit_enabled_p())
7373

7474
enum context_type {
7575
CONTINUATION_CONTEXT = 0,

test/ruby/test_yjit.rb

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -825,12 +825,126 @@ def foo
825825
RUBY
826826
end
827827

828+
def test_code_gc
829+
assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok)
830+
return :not_paged unless add_pages(100) # prepare freeable pages
831+
code_gc # first code GC
832+
return :not_compiled1 unless compiles { nil } # should be JITable again
833+
834+
code_gc # second code GC
835+
return :not_compiled2 unless compiles { nil } # should be JITable again
836+
837+
code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
838+
return :"code_gc_#{code_gc_count}" if code_gc_count && code_gc_count != 2
839+
840+
:ok
841+
RUBY
842+
end
843+
844+
def test_on_stack_code_gc_call
845+
assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok)
846+
fiber = Fiber.new {
847+
# Loop to call the same basic block again after Fiber.yield
848+
while true
849+
Fiber.yield(nil.to_i)
850+
end
851+
}
852+
853+
return :not_paged1 unless add_pages(400) # go to a page without initial ocb code
854+
return :broken_resume1 if fiber.resume != 0 # JIT the fiber
855+
code_gc # first code GC, which should not free the fiber page
856+
return :broken_resume2 if fiber.resume != 0 # The code should be still callable
857+
858+
code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
859+
return :"code_gc_#{code_gc_count}" if code_gc_count && code_gc_count != 1
860+
861+
:ok
862+
RUBY
863+
end
864+
865+
def test_on_stack_code_gc_twice
866+
assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok)
867+
fiber = Fiber.new {
868+
# Loop to call the same basic block again after Fiber.yield
869+
while Fiber.yield(nil.to_i); end
870+
}
871+
872+
return :not_paged1 unless add_pages(400) # go to a page without initial ocb code
873+
return :broken_resume1 if fiber.resume(true) != 0 # JIT the fiber
874+
code_gc # first code GC, which should not free the fiber page
875+
876+
return :not_paged2 unless add_pages(300) # add some stuff to be freed
877+
# Not calling fiber.resume here to test the case that the YJIT payload loses some
878+
# information at the previous code GC. The payload should still be there, and
879+
# thus we could know the fiber ISEQ is still on stack on this second code GC.
880+
code_gc # second code GC, which should still not free the fiber page
881+
882+
return :not_paged3 unless add_pages(200) # attempt to overwrite the fiber page (it shouldn't)
883+
return :broken_resume2 if fiber.resume(true) != 0 # The fiber code should be still fine
884+
885+
return :broken_resume3 if fiber.resume(false) != nil # terminate the fiber
886+
code_gc # third code GC, freeing a page that used to be on stack
887+
888+
return :not_paged4 unless add_pages(100) # check everything still works
889+
890+
code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
891+
return :"code_gc_#{code_gc_count}" if code_gc_count && code_gc_count != 3
892+
893+
:ok
894+
RUBY
895+
end
896+
897+
def test_code_gc_with_many_iseqs
898+
assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok, mem_size: 1)
899+
fiber = Fiber.new {
900+
# Loop to call the same basic block again after Fiber.yield
901+
while true
902+
Fiber.yield(nil.to_i)
903+
end
904+
}
905+
906+
return :not_paged1 unless add_pages(500) # use some pages
907+
return :broken_resume1 if fiber.resume != 0 # leave an on-stack code as well
908+
909+
add_pages(2000) # use a whole lot of pages to run out of 1MiB
910+
return :broken_resume2 if fiber.resume != 0 # on-stack code should be callable
911+
912+
code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
913+
return :"code_gc_#{code_gc_count}" if code_gc_count && code_gc_count == 0
914+
915+
:ok
916+
RUBY
917+
end
918+
919+
private
920+
921+
def code_gc_helpers
922+
<<~'RUBY'
923+
def compiles(&block)
924+
failures = RubyVM::YJIT.runtime_stats[:compilation_failure]
925+
block.call
926+
failures == RubyVM::YJIT.runtime_stats[:compilation_failure]
927+
end
928+
929+
def add_pages(num_jits)
930+
pages = RubyVM::YJIT.runtime_stats[:compiled_page_count]
931+
num_jits.times { return false unless eval('compiles { nil.to_i }') }
932+
pages.nil? || pages < RubyVM::YJIT.runtime_stats[:compiled_page_count]
933+
end
934+
935+
def code_gc
936+
RubyVM::YJIT.simulate_oom! # bump write_pos
937+
eval('proc { nil }.call') # trigger code GC
938+
end
939+
RUBY
940+
end
941+
828942
def assert_no_exits(script)
829943
assert_compiles(script)
830944
end
831945

832946
ANY = Object.new
833-
def assert_compiles(test_script, insns: [], call_threshold: 1, stdout: nil, exits: {}, result: ANY, frozen_string_literal: nil)
947+
def assert_compiles(test_script, insns: [], call_threshold: 1, stdout: nil, exits: {}, result: ANY, frozen_string_literal: nil, mem_size: nil)
834948
reset_stats = <<~RUBY
835949
RubyVM::YJIT.runtime_stats
836950
RubyVM::YJIT.reset_stats!
@@ -864,7 +978,7 @@ def collect_insns(iseq)
864978
#{write_results}
865979
RUBY
866980

867-
status, out, err, stats = eval_with_jit(script, call_threshold: call_threshold)
981+
status, out, err, stats = eval_with_jit(script, call_threshold:, mem_size:)
868982

869983
assert status.success?, "exited with status #{status.to_i}, stderr:\n#{err}"
870984

@@ -918,12 +1032,13 @@ def script_shell_encode(s)
9181032
s.chars.map { |c| c.ascii_only? ? c : "\\u%x" % c.codepoints[0] }.join
9191033
end
9201034

921-
def eval_with_jit(script, call_threshold: 1, timeout: 1000)
1035+
def eval_with_jit(script, call_threshold: 1, timeout: 1000, mem_size: nil)
9221036
args = [
9231037
"--disable-gems",
9241038
"--yjit-call-threshold=#{call_threshold}",
9251039
"--yjit-stats"
9261040
]
1041+
args << "--yjit-exec-mem-size=#{mem_size}" if mem_size
9271042
args << "-e" << script_shell_encode(script)
9281043
stats_r, stats_w = IO.pipe
9291044
out, err, status = EnvUtil.invoke_ruby(args,

yjit.c

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "probes_helper.h"
2828
#include "iseq.h"
2929
#include "ruby/debug.h"
30+
#include "internal/cont.h"
3031

3132
// For mmapp(), sysconf()
3233
#ifndef _WIN32
@@ -65,10 +66,7 @@ STATIC_ASSERT(pointer_tagging_scheme, USE_FLONUM);
6566
bool
6667
rb_yjit_mark_writable(void *mem_block, uint32_t mem_size)
6768
{
68-
if (mprotect(mem_block, mem_size, PROT_READ | PROT_WRITE)) {
69-
return false;
70-
}
71-
return true;
69+
return mprotect(mem_block, mem_size, PROT_READ | PROT_WRITE) == 0;
7270
}
7371

7472
void
@@ -85,6 +83,20 @@ rb_yjit_mark_executable(void *mem_block, uint32_t mem_size)
8583
}
8684
}
8785

86+
// Free the specified memory block.
87+
bool
88+
rb_yjit_mark_unused(void *mem_block, uint32_t mem_size)
89+
{
90+
// On Linux, you need to use madvise MADV_DONTNEED to free memory.
91+
// We might not need to call this on macOS, but it's not really documented.
92+
// We generally prefer to do the same thing on both to ease testing too.
93+
madvise(mem_block, mem_size, MADV_DONTNEED);
94+
95+
// On macOS, mprotect PROT_NONE seems to reduce RSS.
96+
// We also call this on Linux to avoid executing unused pages.
97+
return mprotect(mem_block, mem_size, PROT_NONE) == 0;
98+
}
99+
88100
// `start` is inclusive and `end` is exclusive.
89101
void
90102
rb_yjit_icache_invalidate(void *start, void *end)
@@ -387,6 +399,9 @@ rb_iseq_reset_jit_func(const rb_iseq_t *iseq)
387399
{
388400
RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq));
389401
iseq->body->jit_func = NULL;
402+
// Enable re-compiling this ISEQ. Event when it's invalidated for TracePoint,
403+
// we'd like to re-compile ISEQs that haven't been converted to trace_* insns.
404+
iseq->body->total_calls = 0;
390405
}
391406

392407
// Get the PC for a given index in an iseq

yjit.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,17 @@ def _print_stats
212212
$stderr.puts "bindings_allocations: " + ("%10d" % stats[:binding_allocations])
213213
$stderr.puts "bindings_set: " + ("%10d" % stats[:binding_set])
214214
$stderr.puts "compilation_failure: " + ("%10d" % compilation_failure) if compilation_failure != 0
215-
$stderr.puts "compiled_iseq_count: " + ("%10d" % stats[:compiled_iseq_count])
216215
$stderr.puts "compiled_block_count: " + ("%10d" % stats[:compiled_block_count])
216+
$stderr.puts "compiled_iseq_count: " + ("%10d" % stats[:compiled_iseq_count])
217+
$stderr.puts "compiled_page_count: " + ("%10d" % stats[:compiled_page_count])
217218
$stderr.puts "freed_iseq_count: " + ("%10d" % stats[:freed_iseq_count])
219+
$stderr.puts "freed_page_count: " + ("%10d" % stats[:freed_page_count])
218220
$stderr.puts "invalidation_count: " + ("%10d" % stats[:invalidation_count])
219221
$stderr.puts "constant_state_bumps: " + ("%10d" % stats[:constant_state_bumps])
220222
$stderr.puts "inline_code_size: " + ("%10d" % stats[:inline_code_size])
221223
$stderr.puts "outlined_code_size: " + ("%10d" % stats[:outlined_code_size])
224+
$stderr.puts "freed_code_size: " + ("%10d" % stats[:freed_code_size])
225+
$stderr.puts "code_gc_count: " + ("%10d" % stats[:code_gc_count])
222226
$stderr.puts "num_gc_obj_refs: " + ("%10d" % stats[:num_gc_obj_refs])
223227

224228
$stderr.puts "total_exit_count: " + ("%10d" % total_exits)

yjit/bindgen/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ fn main() {
263263
.allowlist_function("rb_yjit_reserve_addr_space")
264264
.allowlist_function("rb_yjit_mark_writable")
265265
.allowlist_function("rb_yjit_mark_executable")
266+
.allowlist_function("rb_yjit_mark_unused")
266267
.allowlist_function("rb_yjit_get_page_size")
267268
.allowlist_function("rb_leaf_invokebuiltin_iseq_p")
268269
.allowlist_function("rb_leaf_builtin_function")
@@ -297,6 +298,9 @@ fn main() {
297298
// From internal/compile.h
298299
.allowlist_function("rb_vm_insn_decode")
299300

301+
// from internal/cont.h
302+
.allowlist_function("rb_jit_cont_each_iseq")
303+
300304
// From iseq.h
301305
.allowlist_function("rb_vm_insn_addr2opcode")
302306
.allowlist_function("rb_iseqw_to_iseq")

0 commit comments

Comments
 (0)