From 53bc08564542206d0f93dfe984059e245ad16d46 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 25 Aug 2026 00:50:55 +1200 Subject: [PATCH 1/7] Expose `IO::Buffer` mapping alignment. (#18474) [Feature #21700] --- include/ruby/io/buffer.h | 3 ++ io_buffer.c | 31 ++++++++++++++++---- spec/ruby/core/io/buffer/map_spec.rb | 43 +++++++++++++++++++++++---- test/ruby/test_io_buffer.rb | 44 ++++++++++++++++++++++++---- 4 files changed, 104 insertions(+), 17 deletions(-) diff --git a/include/ruby/io/buffer.h b/include/ruby/io/buffer.h index 0d6b52e2698e82..38c6bfcf3eded3 100644 --- a/include/ruby/io/buffer.h +++ b/include/ruby/io/buffer.h @@ -29,6 +29,9 @@ RUBY_EXTERN VALUE rb_cIOBuffer; // The operating system page size. RUBY_EXTERN size_t RUBY_IO_BUFFER_PAGE_SIZE; +// The alignment required for file mapping offsets. +RUBY_EXTERN size_t RUBY_IO_BUFFER_MAP_ALIGNMENT; + // The default buffer size, usually a (small) multiple of the page size. // Can be overridden by the RUBY_IO_BUFFER_DEFAULT_SIZE environment variable. RUBY_EXTERN size_t RUBY_IO_BUFFER_DEFAULT_SIZE; diff --git a/io_buffer.c b/io_buffer.c index 54896098751cec..5a5128aa1dd949 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -30,6 +30,7 @@ VALUE rb_eIOBufferInvalidatedError; VALUE rb_eIOBufferMaskError; size_t RUBY_IO_BUFFER_PAGE_SIZE; +size_t RUBY_IO_BUFFER_MAP_ALIGNMENT; size_t RUBY_IO_BUFFER_DEFAULT_SIZE; #ifdef _WIN32 @@ -818,6 +819,19 @@ rb_io_buffer_new_locked(void *base, size_t size, enum rb_io_buffer_flags flags) VALUE rb_io_buffer_map(VALUE io, size_t size, rb_off_t offset, enum rb_io_buffer_flags flags) { + if (UNLIKELY(offset < 0)) { + rb_raise(rb_eArgError, + "Offset (%" PRIsVALUE ") can't be negative!", + OFFT2NUM(offset)); + } + + if (UNLIKELY((uintmax_t)offset % RUBY_IO_BUFFER_MAP_ALIGNMENT != 0)) { + rb_raise(rb_eArgError, + "Offset (%" PRIsVALUE ") must be a multiple of IO::Buffer::MAP_ALIGNMENT (%" PRIuSIZE ")!", + OFFT2NUM(offset), + RUBY_IO_BUFFER_MAP_ALIGNMENT); + } + VALUE instance = rb_io_buffer_type_allocate(rb_cIOBuffer); struct rb_io_buffer *buffer = get_io_buffer(instance); @@ -835,9 +849,10 @@ rb_io_buffer_map(VALUE io, size_t size, rb_off_t offset, enum rb_io_buffer_flags * Create an IO::Buffer for reading from +file+ by memory-mapping the file. * +file+ should be a +File+ instance, opened for reading or reading and writing. * - * Optional +size+ and +offset+ of mapping can be specified. - * Trying to map an empty file or specify +size+ of 0 will raise an error. - * Valid values for +offset+ are system-dependent. + * Optional +size+ and +offset+ of mapping can be specified. The +offset+ must + * be a multiple of IO::Buffer::MAP_ALIGNMENT. The +size+ does not need to be + * aligned. Trying to map an empty file or specify +size+ of 0 will raise an + * error. * * By default, the buffer is writable and expects the file to be writable. * It is also shared, so several processes can use the same mapping. @@ -937,10 +952,9 @@ io_buffer_map(int argc, VALUE *argv, VALUE klass) size = (size_t)(file_size - offset); } else if (UNLIKELY((size_t)(file_size - offset) < size)) { - size_t maximum_page_count = - (file_size - size) / RUBY_IO_BUFFER_PAGE_SIZE; size_t maximum_offset = - RUBY_IO_BUFFER_PAGE_SIZE * maximum_page_count; + (file_size - size) / RUBY_IO_BUFFER_MAP_ALIGNMENT * + RUBY_IO_BUFFER_MAP_ALIGNMENT; rb_raise(rb_eArgError, "Offset (%" PRIsVALUE ") can't be larger than " "%" PRIuSIZE " for requested size (%" PRIuSIZE ")", @@ -4353,8 +4367,10 @@ Init_IO_Buffer(void) SYSTEM_INFO info; GetSystemInfo(&info); RUBY_IO_BUFFER_PAGE_SIZE = info.dwPageSize; + RUBY_IO_BUFFER_MAP_ALIGNMENT = info.dwAllocationGranularity; #else /* not WIN32 */ RUBY_IO_BUFFER_PAGE_SIZE = sysconf(_SC_PAGESIZE); + RUBY_IO_BUFFER_MAP_ALIGNMENT = RUBY_IO_BUFFER_PAGE_SIZE; #endif RUBY_IO_BUFFER_DEFAULT_SIZE = io_buffer_default_size(RUBY_IO_BUFFER_PAGE_SIZE); @@ -4362,6 +4378,9 @@ Init_IO_Buffer(void) /* The operating system page size. Used for efficient page-aligned memory allocations. */ rb_define_const(rb_cIOBuffer, "PAGE_SIZE", SIZET2NUM(RUBY_IO_BUFFER_PAGE_SIZE)); + /* The alignment required for file mapping offsets. Mapping sizes do not need to be aligned. */ + rb_define_const(rb_cIOBuffer, "MAP_ALIGNMENT", SIZET2NUM(RUBY_IO_BUFFER_MAP_ALIGNMENT)); + /* The default buffer size, typically a (small) multiple of the PAGE_SIZE. Can be explicitly specified by setting the RUBY_IO_BUFFER_DEFAULT_SIZE environment variable. */ diff --git a/spec/ruby/core/io/buffer/map_spec.rb b/spec/ruby/core/io/buffer/map_spec.rb index a711271fd54599..5781c731fbc75b 100644 --- a/spec/ruby/core/io/buffer/map_spec.rb +++ b/spec/ruby/core/io/buffer/map_spec.rb @@ -31,6 +31,15 @@ def open_big_file_fixture File.open(@big_file_name, "rb+") end + def open_map_aligned_file_fixture + unless @map_aligned_file_name + @map_aligned_file_name = tmp("map_aligned_file") + File.write(@map_aligned_file_name, "12345678" * (IO::Buffer::MAP_ALIGNMENT / 8 + 2)) + @tmp_files << @map_aligned_file_name + end + File.open(@map_aligned_file_name, "rb+") + end + after :each do @buffer&.free @buffer = nil @@ -179,8 +188,32 @@ def open_big_file_fixture end context "with size and offset arguments" do - # Neither Windows nor macOS have clear, stable behavior with non-zero offset. - # https://bugs.ruby-lang.org/issues/21700 + ruby_version_is "4.1" do + it "maps a file from an offset aligned to MAP_ALIGNMENT" do + @file = open_map_aligned_file_fixture + @buffer = IO::Buffer.map(@file, 14, IO::Buffer::MAP_ALIGNMENT) + + @buffer.size.should == 14 + @buffer.get_string(0, 14).should == "12345678123456" + end + + it "maps the rest of a file from an offset aligned to MAP_ALIGNMENT" do + @file = open_map_aligned_file_fixture + @buffer = IO::Buffer.map(@file, nil, IO::Buffer::MAP_ALIGNMENT) + + @buffer.get_string(0, 1).should == "1" + @buffer.size.should == (@file.size - IO::Buffer::MAP_ALIGNMENT) + end + + it "raises ArgumentError if offset is not aligned to MAP_ALIGNMENT" do + @file = open_fixture + message = "Offset (1) must be a multiple of IO::Buffer::MAP_ALIGNMENT (#{IO::Buffer::MAP_ALIGNMENT})!" + + -> { IO::Buffer.map(@file, 1, 1) }.should.raise(ArgumentError, message) + end + end + + # Before MAP_ALIGNMENT was exposed, these offsets were only portable to Linux. platform_is :linux do context "if offset is an allowed value for system call" do it "maps the span specified by size starting from the offset" do @@ -252,10 +285,10 @@ def open_big_file_fixture ruby_version_is "4.1" do it "raises ArgumentError if offset+size is larger than file size" do - @file = open_big_file_fixture + @file = open_map_aligned_file_fixture size = 17 - maximum_page_size = 0 - -> { IO::Buffer.map(@file, size, IO::Buffer::PAGE_SIZE) }.should.raise(ArgumentError, "Offset (#{IO::Buffer::PAGE_SIZE}) can't be larger than #{maximum_page_size} for requested size (#{size})") + maximum_offset = 0 + -> { IO::Buffer.map(@file, size, IO::Buffer::MAP_ALIGNMENT) }.should.raise(ArgumentError, "Offset (#{IO::Buffer::MAP_ALIGNMENT}) can't be larger than #{maximum_offset} for requested size (#{size})") ensure # Windows requires the file to be closed before deletion. @file.close unless @file.closed? diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 1e6baac2710242..500ca5b7eb0b56 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -32,6 +32,11 @@ def test_flags assert_equal 128, IO::Buffer::READONLY end + def test_map_alignment + assert_kind_of Integer, IO::Buffer::MAP_ALIGNMENT + assert_positive IO::Buffer::MAP_ALIGNMENT + end + def test_internal_for_reading_with_string string = "hello" @@ -211,6 +216,33 @@ def test_file_mapped_with_size assert_equal Encoding::BINARY, contents.encoding end + def test_file_mapped_with_aligned_offset + alignment = IO::Buffer::MAP_ALIGNMENT + + Tempfile.create do |file| + file.binmode + file.write("\0" * alignment) + file.write("test") + file.flush + + buffer = IO::Buffer.map(file, 4, alignment, IO::Buffer::READONLY) + assert_equal "test", buffer.get_string + ensure + buffer&.free + end + end + + def test_file_mapped_with_unaligned_offset + alignment = IO::Buffer::MAP_ALIGNMENT + message = "Offset (1) must be a multiple of IO::Buffer::MAP_ALIGNMENT (#{alignment})!" + + File.open(__FILE__) do |file| + assert_raise_with_message(ArgumentError, message) do + IO::Buffer.map(file, 1, 1, IO::Buffer::READONLY) + end + end + end + def test_file_mapped_size_too_large size = File.size(__FILE__) + 1 file_size = File.size(__FILE__) @@ -236,19 +268,19 @@ def test_file_mapped_offset_negative def test_file_mapped_offset_too_large file_size = File.size(__FILE__) - page_count = file_size / IO::Buffer::PAGE_SIZE - offset = IO::Buffer::PAGE_SIZE * (page_count + 1) + alignment_count = file_size / IO::Buffer::MAP_ALIGNMENT + offset = IO::Buffer::MAP_ALIGNMENT * (alignment_count + 1) message = "Offset (#{offset}) can't be larger than file size (#{file_size})" assert_raise_with_message ArgumentError, message do File.open(__FILE__) {|file| IO::Buffer.map(file, nil, offset, IO::Buffer::READONLY)} end - if page_count > 0 - offset = IO::Buffer::PAGE_SIZE * page_count + if alignment_count > 0 + offset = IO::Buffer::MAP_ALIGNMENT * alignment_count available_size = file_size - offset size = available_size + 1 - maximum_page_count = (file_size - size) / IO::Buffer::PAGE_SIZE - maximum_offset = IO::Buffer::PAGE_SIZE * maximum_page_count + maximum_alignment_count = (file_size - size) / IO::Buffer::MAP_ALIGNMENT + maximum_offset = IO::Buffer::MAP_ALIGNMENT * maximum_alignment_count message = "Offset (#{offset}) can't be larger than #{maximum_offset} " + "for requested size (#{size})" assert_raise_with_message ArgumentError, message do From b548f16e303f3336309f84e0fd0bb935349e08b1 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Mon, 24 Aug 2026 11:23:11 +0900 Subject: [PATCH 2/7] Make union for positions and succ_index_table in rb_iseq_constant_body We only use either positions and succ_index_table (when VM_INSN_INFO_TABLE_IMPL == 2, positions is converted to succ_index_table), so we waste 8 bytes in rb_iseq_constant_body. This changes it so that they are in a union. --- .gdbinit | 58 +++++++++++++--------------------- compile.c | 12 +++---- iseq.c | 46 +++++++++++++-------------- vm_core.h | 8 +++-- zjit/src/cruby_bindings.inc.rs | 11 +++++-- 5 files changed, 62 insertions(+), 73 deletions(-) diff --git a/.gdbinit b/.gdbinit index 4457f6f12b087d..f045b12fb0f913 100644 --- a/.gdbinit +++ b/.gdbinit @@ -983,49 +983,35 @@ define print_lineno set $index = 0 set $size = $iseq->body->insns_info.size set $table = $iseq->body->insns_info.body - set $positions = $iseq->body->insns_info.positions #printf "size: %d\n", $size if $size == 0 else if $size == 1 printf "%d", $table[0].line_no else - if $positions - # get_insn_info_linear_search - set $index = 1 - while $index < $size - #printf "table[%d]: position: %d, line: %d, pos: %d\n", $i, $positions[$i], $table[$i].line_no, $pos - if $positions[$index] > $pos - loop_break - end - set $index = $index + 1 - if $positions[$index] == $pos - loop_break - end - end + # get_insn_info_succinct_bitvector (VM_INSN_INFO_TABLE_IMPL == 2). + # insns_info.positions and insns_info.succ_index_table share a union, and + # an iseq running on the stack always has succ_index_table. + set $sd = $iseq->body->insns_info.positions_or_succ_index_table.succ_index_table + set $immediate_table_size = sizeof($sd->imm_part) / sizeof(uint64_t) * 9 + if $pos < $immediate_table_size + set $i = $pos / 9 + set $j = $pos % 9 + set $index = ((int)($sd->imm_part[$i] >> ($j * 7))) & 0x7f else - # get_insn_info_succinct_bitvector - set $sd = $iseq->body->insns_info.succ_index_table - set $immediate_table_size = sizeof($sd->imm_part) / sizeof(uint64_t) * 9 - if $pos < $immediate_table_size - set $i = $pos / 9 - set $j = $pos % 9 - set $index = ((int)($sd->imm_part[$i] >> ($j * 7))) & 0x7f - else - set $block_index = ($pos - $immediate_table_size) / 512 - set $block = &$sd->succ_part[$block_index] - set $block_bit_index = ($pos - $immediate_table_size) % 512 - set $small_block_index = $block_bit_index / 64 - set $small_block_popcount = $small_block_index == 0 ? 0 : (((int)($block->small_block_ranks >> (($small_block_index - 1) * 9))) & 0x1ff) - set $x = $block->bits[$small_block_index] << (63 - $block_bit_index % 64) - set $x = ($x & 0x5555555555555555) + ($x >> 1 & 0x5555555555555555) - set $x = ($x & 0x3333333333333333) + ($x >> 2 & 0x3333333333333333) - set $x = ($x & 0x0707070707070707) + ($x >> 4 & 0x0707070707070707) - set $x = ($x & 0x001f001f001f001f) + ($x >> 8 & 0x001f001f001f001f) - set $x = ($x & 0x0000003f0000003f) + ($x >>16 & 0x0000003f0000003f) - set $popcnt = ($x & 0x7f) + ($x >>32 & 0x7f) - set $index = $block->rank + $small_block_popcount + $popcnt - end + set $block_index = ($pos - $immediate_table_size) / 512 + set $block = &$sd->succ_part[$block_index] + set $block_bit_index = ($pos - $immediate_table_size) % 512 + set $small_block_index = $block_bit_index / 64 + set $small_block_popcount = $small_block_index == 0 ? 0 : (((int)($block->small_block_ranks >> (($small_block_index - 1) * 9))) & 0x1ff) + set $x = $block->bits[$small_block_index] << (63 - $block_bit_index % 64) + set $x = ($x & 0x5555555555555555) + ($x >> 1 & 0x5555555555555555) + set $x = ($x & 0x3333333333333333) + ($x >> 2 & 0x3333333333333333) + set $x = ($x & 0x0707070707070707) + ($x >> 4 & 0x0707070707070707) + set $x = ($x & 0x001f001f001f001f) + ($x >> 8 & 0x001f001f001f001f) + set $x = ($x & 0x0000003f0000003f) + ($x >>16 & 0x0000003f0000003f) + set $popcnt = ($x & 0x7f) + ($x >>32 & 0x7f) + set $index = $block->rank + $small_block_popcount + $popcnt end printf "%d", $table[$index-1].line_no end diff --git a/compile.c b/compile.c index 52f64ce0ae1e93..73a1065141219b 100644 --- a/compile.c +++ b/compile.c @@ -1700,10 +1700,8 @@ iseq_setup(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) } #if VM_INSN_INFO_TABLE_IMPL == 2 - if (ISEQ_BODY(iseq)->insns_info.succ_index_table == NULL) { - debugs("[compile step 7 (rb_iseq_insns_info_encode_positions)] \n"); - rb_iseq_insns_info_encode_positions(iseq); - } + debugs("[compile step 7 (rb_iseq_insns_info_encode_positions)] \n"); + rb_iseq_insns_info_encode_positions(iseq); #endif if (compile_debug > 1) { @@ -2976,12 +2974,12 @@ iseq_set_sequence(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) /* get rid of memory leak when REALLOC failed */ body->insns_info.body = insns_info; - body->insns_info.positions = positions; + body->insns_info.positions_or_succ_index_table.positions = positions; SIZED_REALLOC_N(insns_info, struct iseq_insn_info_entry, insns_info_index, insns_info_size); body->insns_info.body = insns_info; SIZED_REALLOC_N(positions, unsigned int, insns_info_index, positions_size); - body->insns_info.positions = positions; + body->insns_info.positions_or_succ_index_table.positions = positions; body->insns_info.size = insns_info_index; return COMPILE_OK; @@ -14044,7 +14042,7 @@ ibf_load_iseq_each(struct ibf_load *load, rb_iseq_t *iseq, ibf_offset_t offset) load_body->param.keyword = ibf_load_param_keyword(load, param_keyword_offset); load_body->param.flags.has_kw = (param_flags >> 4) & 1; load_body->insns_info.body = ibf_load_insns_info_body(load, insns_info_body_offset, insns_info_size); - load_body->insns_info.positions = ibf_load_insns_info_positions(load, insns_info_positions_offset, insns_info_size); + load_body->insns_info.positions_or_succ_index_table.positions = ibf_load_insns_info_positions(load, insns_info_positions_offset, insns_info_size); load_body->local_table = ibf_load_local_table(load, local_table_offset, local_table_size); load_body->lvar_states = ibf_load_lvar_states(load, lvar_states_offset, local_table_size, load_body->local_table); ibf_load_catch_table(load, catch_table_offset, catch_table_size, iseq); diff --git a/iseq.c b/iseq.c index 80d7324a1ec088..27bad3e7d09e0d 100644 --- a/iseq.c +++ b/iseq.c @@ -203,9 +203,10 @@ rb_iseq_free(const rb_iseq_t *iseq) #endif SIZED_FREE_N(body->iseq_encoded, body->iseq_size); SIZED_FREE_N(body->insns_info.body, body->insns_info.size); - SIZED_FREE_N(body->insns_info.positions, body->insns_info.size); #if VM_INSN_INFO_TABLE_IMPL == 2 - ruby_xfree(body->insns_info.succ_index_table); + ruby_xfree(body->insns_info.positions_or_succ_index_table.succ_index_table); +#else + SIZED_FREE_N(body->insns_info.positions_or_succ_index_table.positions, body->insns_info.size); #endif SIZED_FREE_N(body->is_entries, ISEQ_IS_SIZE(body)); SIZED_FREE_N(body->call_data, body->ci_size); @@ -806,13 +807,10 @@ rb_iseq_insns_info_encode_positions(const rb_iseq_t *iseq) struct rb_iseq_constant_body *const body = ISEQ_BODY(iseq); int size = body->insns_info.size; int max_pos = body->iseq_size; - int *data = (int *)body->insns_info.positions; - if (body->insns_info.succ_index_table) ruby_xfree(body->insns_info.succ_index_table); - body->insns_info.succ_index_table = succ_index_table_create(max_pos, data, size); -#if VM_CHECK_MODE == 0 - SIZED_FREE_N(body->insns_info.positions, body->insns_info.size); - body->insns_info.positions = NULL; -#endif + unsigned int *positions = body->insns_info.positions_or_succ_index_table.positions; + struct succ_index_table *sd = succ_index_table_create(max_pos, (int *)positions, size); + SIZED_FREE_N(positions, size); + body->insns_info.positions_or_succ_index_table.succ_index_table = sd; #endif } @@ -822,7 +820,7 @@ rb_iseq_insns_info_decode_positions(const struct rb_iseq_constant_body *body) { int size = body->insns_info.size; int max_pos = body->iseq_size; - struct succ_index_table *sd = body->insns_info.succ_index_table; + struct succ_index_table *sd = body->insns_info.positions_or_succ_index_table.succ_index_table; return succ_index_table_invert(max_pos, sd, size); } #endif @@ -2364,7 +2362,7 @@ get_insn_info_binary_search(const rb_iseq_t *iseq, size_t pos) const struct rb_iseq_constant_body *const body = ISEQ_BODY(iseq); size_t size = body->insns_info.size; const struct iseq_insn_info_entry *insns_info = body->insns_info.body; - const unsigned int *positions = body->insns_info.positions; + const unsigned int *positions = body->insns_info.positions_or_succ_index_table.positions; const int debug = 0; if (debug) { @@ -2420,16 +2418,9 @@ get_insn_info_succinct_bitvector(const rb_iseq_t *iseq, size_t pos) const int debug = 0; if (debug) { -#if VM_CHECK_MODE > 0 - const unsigned int *positions = body->insns_info.positions; - printf("size: %"PRIuSIZE"\n", size); - printf("insns_info[%"PRIuSIZE"]: position: %d, line: %d, pos: %"PRIuSIZE"\n", - (size_t)0, positions[0], insns_info[0].line_no, pos); -#else printf("size: %"PRIuSIZE"\n", size); printf("insns_info[%"PRIuSIZE"]: line: %d, pos: %"PRIuSIZE"\n", (size_t)0, insns_info[0].line_no, pos); -#endif } if (size == 0) { @@ -2440,8 +2431,8 @@ get_insn_info_succinct_bitvector(const rb_iseq_t *iseq, size_t pos) } else { int index; - VM_ASSERT(body->insns_info.succ_index_table != NULL); - index = succ_index_lookup(body->insns_info.succ_index_table, (int)pos); + VM_ASSERT(body->insns_info.positions_or_succ_index_table.succ_index_table != NULL); + index = succ_index_lookup(body->insns_info.positions_or_succ_index_table.succ_index_table, (int)pos); return &insns_info[index-1]; } } @@ -2455,12 +2446,11 @@ get_insn_info(const rb_iseq_t *iseq, size_t pos) #if VM_CHECK_MODE > 0 || VM_INSN_INFO_TABLE_IMPL == 0 static const struct iseq_insn_info_entry * -get_insn_info_linear_search(const rb_iseq_t *iseq, size_t pos) +get_insn_info_linear_search(const rb_iseq_t *iseq, const unsigned int *positions, size_t pos) { const struct rb_iseq_constant_body *const body = ISEQ_BODY(iseq); size_t i = 0, size = body->insns_info.size; const struct iseq_insn_info_entry *insns_info = body->insns_info.body; - const unsigned int *positions = body->insns_info.positions; const int debug = 0; if (debug) { @@ -2496,7 +2486,7 @@ get_insn_info_linear_search(const rb_iseq_t *iseq, size_t pos) static const struct iseq_insn_info_entry * get_insn_info(const rb_iseq_t *iseq, size_t pos) { - return get_insn_info_linear_search(iseq, pos); + return get_insn_info_linear_search(iseq, ISEQ_BODY(iseq)->insns_info.positions_or_succ_index_table.positions, pos); } #endif @@ -2506,11 +2496,19 @@ validate_get_insn_info(const rb_iseq_t *iseq) { const struct rb_iseq_constant_body *const body = ISEQ_BODY(iseq); size_t i; +#if VM_INSN_INFO_TABLE_IMPL == 2 + unsigned int *positions = rb_iseq_insns_info_decode_positions(body); +#else + const unsigned int *positions = body->insns_info.positions_or_succ_index_table.positions; +#endif for (i = 0; i < body->iseq_size; i++) { - if (get_insn_info_linear_search(iseq, i) != get_insn_info(iseq, i)) { + if (get_insn_info_linear_search(iseq, positions, i) != get_insn_info(iseq, i)) { rb_bug("validate_get_insn_info: get_insn_info_linear_search(iseq, %"PRIuSIZE") != get_insn_info(iseq, %"PRIuSIZE")", i, i); } } +#if VM_INSN_INFO_TABLE_IMPL == 2 + SIZED_FREE_N(positions, body->insns_info.size); +#endif } #endif diff --git a/vm_core.h b/vm_core.h index 3dbfe7442a7f1d..6b86c9b3e81c12 100644 --- a/vm_core.h +++ b/vm_core.h @@ -504,11 +504,13 @@ struct rb_iseq_constant_body { /* insn info, must be freed */ struct iseq_insn_info { const struct iseq_insn_info_entry *body; - unsigned int *positions; - unsigned int size; + union { + unsigned int *positions; #if VM_INSN_INFO_TABLE_IMPL == 2 - struct succ_index_table *succ_index_table; + struct succ_index_table *succ_index_table; #endif + } positions_or_succ_index_table; + unsigned int size; } insns_info; const ID *local_table; /* must free */ diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 437121381c7009..ebd8359e798c1c 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1260,11 +1260,16 @@ pub struct rb_iseq_constant_body_rb_iseq_parameters_rb_iseq_param_keyword { pub default_values: *mut VALUE, } #[repr(C)] -#[derive(Debug, Copy, Clone)] +#[derive(Copy, Clone)] pub struct rb_iseq_constant_body_iseq_insn_info { pub body: *const iseq_insn_info_entry, - pub positions: *mut ::std::os::raw::c_uint, + pub positions_or_succ_index_table: rb_iseq_constant_body_iseq_insn_info__bindgen_ty_1, pub size: ::std::os::raw::c_uint, +} +#[repr(C)] +#[derive(Copy, Clone)] +pub union rb_iseq_constant_body_iseq_insn_info__bindgen_ty_1 { + pub positions: *mut ::std::os::raw::c_uint, pub succ_index_table: *mut succ_index_table, } #[repr(C)] @@ -2094,7 +2099,7 @@ pub struct zjit_jit_frame { pub stack: __IncompleteArrayField, } pub const ISEQ_BODY_OFFSET_PARAM: zjit_struct_offsets = 16; -pub const ISEQ_BODY_OFFSET_OUTER_VARIABLES: zjit_struct_offsets = 288; +pub const ISEQ_BODY_OFFSET_OUTER_VARIABLES: zjit_struct_offsets = 280; pub const RUBY_OFFSET_THREAD_RACTOR: zjit_struct_offsets = 24; pub type zjit_struct_offsets = u32; #[repr(C)] From 065a5592d2205105e7cbfcfe0bca2e51d1dc7573 Mon Sep 17 00:00:00 2001 From: Ojus Chugh <79078267+ojuschugh1@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:33:36 +0530 Subject: [PATCH 3/7] [ruby/json] Add JSON::ParserError#json_path (https://github.com/ruby/json/pull/1062) Add JSON::ParserError#json_path https://github.com/ruby/json/commit/9ae76bdd3d Co-authored-by: Jean Boussier --- ext/json/lib/json/common.rb | 51 ++++++++++++++++++++- ext/json/parser/parser.c | 53 ++++++++++++++++++---- test/json/json_parser_test.rb | 85 +++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 10 deletions(-) diff --git a/ext/json/lib/json/common.rb b/ext/json/lib/json/common.rb index 0512d4aefd35e2..34dc123b5c0f31 100644 --- a/ext/json/lib/json/common.rb +++ b/ext/json/lib/json/common.rb @@ -142,7 +142,56 @@ class JSONError < StandardError; end # This exception is raised if a parser error occurs. class ParserError < JSONError - attr_reader :line, :column + # Line number where the parser encountered an error. + # Is nil when raised by JSON::ResumableParser. + attr_reader :line + + # Column number where the parser encountered an error. + # Is nil when raised by JSON::ResumableParser. + attr_reader :column + + # Returns a best effort JSONPath string representing where in the document + # the parser encountered an error: + # + # begin + # JSON.parse('{"articles": [ { "title": invalid } ]}') + # rescue JSON::ParserError => error + # error.json_path # => "$.articles[0].title" + # end + def json_path + return @json_path if String === @json_path + + if Array === @json_path + path = build_json_path(@json_path) + @json_path = path unless frozen? + return path + end + end + + private + + def build_json_path(segments) + error = false + path = segments.filter_map do |segment| + next if error + + case segment + when Integer + "[#{segment}]" + when String, Symbol + if segment.match?(/\A[a-zA-Z\$\_][a-zA-Z\$\_0-9]*\z/) + ".#{segment}" + else + segment = segment.to_s.gsub(/["\\]/, { '"' => '\\"', '\\' => '\\\\' }) + %{["#{segment}"]} + end + else + error = true + nil + end + end.join + "$#{path}".freeze + end end # This exception is raised if the nesting of parsed data structures is too diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index f10e2260ad7b75..fc6a7bbf62f626 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -5,7 +5,7 @@ static VALUE mJSON, eNestingError, eParserError, Encoding_UTF_8; static VALUE CNaN, CInfinity, CMinusInfinity, JSON_empty_string; -static ID i_new, i_try_convert, i_encode, i_at_line, i_at_column; +static ID i_new, i_try_convert, i_encode, i_at_line, i_at_column, i_at_json_path; #ifndef HAVE_RB_STR_TO_INTERNED_STR static ID i_uminus; #endif @@ -651,11 +651,42 @@ static VALUE build_parse_error_message(const char *format, JSON_ParserState *sta return rb_enc_sprintf(enc_utf8, format, ptr); } +static VALUE json_path_new(JSON_ParserState *state, VALUE duplicate_key) +{ + VALUE path = rb_ary_new_capa(state->current_nesting); + + json_frame_stack *frames = state->frames; + rvalue_stack *values = state->value_stack; + + for (long depth = 1; depth < frames->head; depth++) { + json_frame *frame = &frames->ptr[depth]; + + bool innermost = depth == frames->head - 1; + long child_head = innermost ? values->head : frames->ptr[depth + 1].value_stack_head; + long count = child_head - frame->value_stack_head; + + if (frame->type == JSON_FRAME_ARRAY) { + rb_ary_push(path, LONG2NUM(frame->phase == JSON_PHASE_ARRAY_COMMA ? count - 1 : count)); + } else if (innermost && !UNDEF_P(duplicate_key)) { + rb_ary_push(path, duplicate_key); + } else if (count & 1) { + rb_ary_push(path, values->ptr[child_head - 1]); + } else if (frame->phase == JSON_PHASE_OBJECT_COMMA && count >= 2) { + rb_ary_push(path, values->ptr[child_head - 2]); + } else { + break; + } + } + + return path; +} + static VALUE parse_error_new(JSON_ParserState *state, VALUE message, long line, long column, bool eos) { VALUE exc = rb_exc_new_str(eParserError, message); rb_ivar_set(exc, i_at_line, LONG2NUM(line)); rb_ivar_set(exc, i_at_column, LONG2NUM(column)); + rb_ivar_set(exc, i_at_json_path, json_path_new(state, Qundef)); return exc; } @@ -1199,14 +1230,17 @@ NORETURN(static) void raise_duplicate_key_error(JSON_ParserState *state, VALUE d ); rb_str_concat(message, build_parse_error_message("", state)); + VALUE exc; if (state->parser) { // line and columns can't be accurate in resumable - rb_exc_raise(parse_error_new(state, message, 0, 0, false)); + exc = parse_error_new(state, message, 0, 0, false); } else { long line, column; cursor_position(state, &line, &column); rb_str_catf(message, " at line %ld column %ld", line, column); - rb_exc_raise(parse_error_new(state, message, line, column, false)); + exc = parse_error_new(state, message, line, column, false); } + rb_ivar_set(exc, i_at_json_path, json_path_new(state, duplicate_key)); + rb_exc_raise(exc); } NOINLINE(static) void json_on_duplicate_key(JSON_ParserState *state, JSON_ParserConfig *config, size_t count, const VALUE *pairs) @@ -2122,6 +2156,12 @@ static VALUE cParser_parse(JSON_ParserConfig *config, VALUE src) // the rvalue stack. VALUE result = complete ? *rvalue_stack_peek(state->value_stack, 1) : Qundef; + if (complete) { + json_ensure_eof(state, config); + } else { + raise_eos_error("unexpected end of input", state); + } + // This may be skipped in case of exception, but // it won't cause a leak. rvalue_stack_eagerly_release(value_stack_handle); @@ -2130,12 +2170,6 @@ static VALUE cParser_parse(JSON_ParserConfig *config, VALUE src) RB_GC_GUARD(frame_stack_handle); RB_GC_GUARD(Vsource); - if (complete) { - json_ensure_eof(state, config); - } else { - raise_eos_error("unexpected end of input", state); - } - return result; } @@ -2871,6 +2905,7 @@ void Init_parser(void) i_encode = rb_intern("encode"); i_at_line = rb_intern("@line"); i_at_column = rb_intern("@column"); + i_at_json_path = rb_intern("@json_path"); binary_encindex = rb_ascii8bit_encindex(); utf8_encindex = rb_utf8_encindex(); diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index e7940faa4ee6f4..84839585fcc755 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -847,6 +847,80 @@ def test_parse_error_snippet assert_equal "unexpected character: '@' at line 1 column 1", error.message end + def test_parse_error_json_path + omit "JRuby errors don't contain positions" if RUBY_ENGINE == "jruby" + + assert_parse_error_at "$", "xyz" + assert_parse_error_at "$.a", '{"a": xyz}' + assert_parse_error_at "$[3]", '[1, 2, "hi", xyz]' + assert_parse_error_at "$.a[1].b", '{"a": [1, {"b": xyz}]}' + assert_parse_error_at "$.a", '{"a": 1 xyz}' + assert_parse_error_at "$", '{"a": 1, xyz}' + + assert_parse_error_at "$.a.b.c", '{"a": {"b": {"c"' + assert_parse_error_at "$.a.b.c", '{"a": {"b": {"c":' + assert_parse_error_at "$.a.b", '{"a": {"b": {"c": 1, "d' + + assert_parse_error_at "$[4]", '[1,2,3,4,5' + assert_parse_error_at "$[5]", '[1,2,3,4,5,' + assert_parse_error_at "$[5]", '[1,2,3,4,5,]' + end + + def test_parse_error_json_path_on_load + omit "JRuby errors don't contain positions" if RUBY_ENGINE == "jruby" + + assert_parse_error_at "$" do + JSON.load('{"a": {"b": {"c":', -> (obj) { + if String === obj + BasicObject.new + else + obj + end + }) + end + + assert_parse_error_at "$.a" do + JSON.load('{"a": {"b": {"c":', -> (obj) { + if obj == "b" + BasicObject.new + else + obj + end + }) + end + end + + def test_parse_error_json_path_key_escaping + omit "JRuby errors don't contain positions" if RUBY_ENGINE == "jruby" + + assert_parse_error_at '$["hello world"]', '{"hello world": xyz}' + assert_parse_error_at '$["a\"b"]', '{"a\"b": xyz}' + assert_parse_error_at '$[""]', '{"": xyz}' + assert_parse_error_at '$["あ"]', '{"あ": xyz}' + assert_parse_error_at '$.foo["1x"]', '{"foo": {"1x": xyz}}' + end + + def test_parse_error_json_path_duplicate_key + omit "JRuby errors don't contain positions" if RUBY_ENGINE == "jruby" + + assert_parse_error_at "$.a", '{"a": 1, "a": 2}' + assert_parse_error_at "$.x.a", '{"x": {"a": 1, "b": 2, "a": 3}}' + assert_parse_error_at "$.arr[0].a", '{"arr": [{"a": 1, "a": 2}]}' + assert_parse_error_at "$.x.a", '{"x": {"a": 1, "a": 2}}' + end + + def test_parse_error_json_path_resumable + omit "JSON::ResumableParser not available" unless defined?(JSON::ResumableParser) + + parser = JSON::ResumableParser.new + parser << '{"a": [1, {"b": ' + parser.parse + assert_parse_error_at "$.a[1].b" do + parser << 'xyz' + parser.parse + end + end + def test_parse_leading_slash # ref: https://github.com/ruby/ruby/pull/12598 assert_raise(JSON::ParserError) do @@ -888,4 +962,15 @@ def assert_equal_float(expected, actual, delta = 1e-2) Array === actual and actual = actual.first assert_in_delta(expected, actual, delta) end + + def assert_parse_error_at(path, json = nil) + error = assert_raise(JSON::ParserError) do + if block_given? + yield + else + JSON.parse(json) + end + end + assert_equal path, error.json_path + end end From 90529c255ecebcaaab8582399d624d2dcc77128e Mon Sep 17 00:00:00 2001 From: Ian Ker-Seymer Date: Mon, 24 Aug 2026 14:13:54 -0400 Subject: [PATCH 4/7] =?UTF-8?q?Raise=20YJIT=E2=80=99s=20`case`=20specializ?= =?UTF-8?q?ation=20limit=20to=20256=20(#18374)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raise YJIT case dispatch specialization limit to 256 --- test/ruby/test_yjit.rb | 20 ++++++++++++++ yjit/src/codegen.rs | 20 +++++++------- yjit/src/core.rs | 62 ++++++++++++++++++++++-------------------- yjit/src/log.rs | 2 +- 4 files changed, 64 insertions(+), 40 deletions(-) diff --git a/test/ruby/test_yjit.rb b/test/ruby/test_yjit.rb index 43aae5fa46a695..0ed394fc8ecef5 100644 --- a/test/ruby/test_yjit.rb +++ b/test/ruby/test_yjit.rb @@ -1223,6 +1223,26 @@ def case_dispatch(val) RUBY end + def test_opt_case_dispatch_all_byte_values + branches = 256.times.map { |value| "when #{value} then #{value}" }.join("\n") + assert_compiles(<<~RUBY, exits: :any, result: :ok) + def case_dispatch(value) + case value + #{branches} + end + end + + values = (0...256).to_a + return :wrong_result unless values.map { |value| case_dispatch(value) } == values + + stats = RubyVM::YJIT.runtime_stats + return :early_fallback if stats[:all_stats] && !stats[:num_opt_case_dispatch_megamorphic].zero? + return :wrong_else unless 2.times.map { case_dispatch(256) } == [nil, nil] + return :ok unless stats[:all_stats] + RubyVM::YJIT.runtime_stats[:num_opt_case_dispatch_megamorphic].positive? ? :ok : :missing_fallback + RUBY + end + def test_code_gc assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok) return :not_paged unless add_pages(100) # prepare freeable pages diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index 9483b715a69671..dffb4593c09202 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -2875,7 +2875,7 @@ fn jit_chain_guard( jcc: JCCKinds, jit: &mut JITState, asm: &mut Assembler, - depth_limit: u8, + depth_limit: u16, counter: Counter, ) { let target0_gen_fn = match jcc { @@ -2902,22 +2902,22 @@ fn jit_chain_guard( } // up to 8 different shapes for each -pub const GET_IVAR_MAX_DEPTH: u8 = 8; +pub const GET_IVAR_MAX_DEPTH: u16 = 8; // up to 8 different shapes for each -pub const SET_IVAR_MAX_DEPTH: u8 = 8; +pub const SET_IVAR_MAX_DEPTH: u16 = 8; // hashes and arrays -pub const OPT_AREF_MAX_CHAIN_DEPTH: u8 = 2; +pub const OPT_AREF_MAX_CHAIN_DEPTH: u16 = 2; // expandarray -pub const EXPANDARRAY_MAX_CHAIN_DEPTH: u8 = 4; +pub const EXPANDARRAY_MAX_CHAIN_DEPTH: u16 = 4; // up to 5 different methods for send -pub const SEND_MAX_DEPTH: u8 = 5; +pub const SEND_MAX_DEPTH: u16 = 5; -// up to 20 different offsets for case-when -pub const CASE_WHEN_MAX_DEPTH: u8 = 20; +// Specialize every value of a byte-sized case expression +pub const CASE_WHEN_MAX_DEPTH: u16 = 256; pub const MAX_SPLAT_LENGTH: i32 = 127; @@ -2928,7 +2928,7 @@ pub const MAX_SPLAT_LENGTH: i32 = 127; fn gen_get_ivar( jit: &mut JITState, asm: &mut Assembler, - max_chain_depth: u8, + max_chain_depth: u16, comptime_receiver: VALUE, ivar_name: ID, recv: Opnd, @@ -4982,7 +4982,7 @@ fn jit_guard_known_klass( obj_opnd: Opnd, insn_opnd: YARVOpnd, sample_instance: VALUE, - max_chain_depth: u8, + max_chain_depth: u16, counter: Counter, ) { let known_klass = sample_instance.class_of(); diff --git a/yjit/src/core.rs b/yjit/src/core.rs index d08cd1fb26fac3..bdea7ccceeb8f3 100644 --- a/yjit/src/core.rs +++ b/yjit/src/core.rs @@ -460,8 +460,10 @@ impl fmt::Debug for RegMapping { } } -/// Maximum value of the chain depth (should fit in 5 bits) -const CHAIN_DEPTH_MAX: u8 = 0b11111; // 31 +/// Maximum value of the chain depth (should fit in 9 bits) +const CHAIN_DEPTH_MAX: u16 = 256; +const DEFERRED_FLAG: u8 = 1 << 0; +const RETURN_LANDING_FLAG: u8 = 1 << 1; /// Code generation context /// Contains information we can use to specialize/optimize code @@ -478,14 +480,11 @@ pub struct Context { reg_mapping: RegMapping, // Depth of this block in the sidechain (eg: inline-cache chain) - // 6 bits, max 63 - chain_depth: u8, + // 9 bits, max 256 + chain_depth: u16, - // Whether this code is the target of a JIT-to-JIT Ruby return ([Self::is_return_landing]) - is_return_landing: bool, - - // Whether the compilation of this code has been deferred ([Self::is_deferred]) - is_deferred: bool, + // Return-landing and deferred flags + flags: u8, // Type we track for self self_type: Type, @@ -580,9 +579,9 @@ impl BitVector { self.push_uint(val as u64, 8); } - fn push_u5(&mut self, val: u8) { - assert!(val <= 0b11111); - self.push_uint(val as u64, 5); + fn push_u9(&mut self, val: u16) { + assert!(val <= 0b1_1111_1111); + self.push_uint(val as u64, 9); } fn push_u4(&mut self, val: u8) { @@ -654,8 +653,8 @@ impl BitVector { self.read_uint(bit_idx, 8) as u8 } - fn read_u5(&self, bit_idx: &mut usize) -> u8 { - self.read_uint(bit_idx, 5) as u8 + fn read_u9(&self, bit_idx: &mut usize) -> u16 { + self.read_uint(bit_idx, 9) as u16 } fn read_u4(&self, bit_idx: &mut usize) -> u8 { @@ -1053,17 +1052,17 @@ impl Context { } } - bits.push_bool(self.is_deferred); - bits.push_bool(self.is_return_landing); + bits.push_bool(self.is_deferred()); + bits.push_bool(self.is_return_landing()); // The chain depth is most often 0 or 1 if self.chain_depth < 2 { bits.push_u1(0); - bits.push_u1(self.chain_depth); + bits.push_u1(self.chain_depth.try_into().unwrap()); } else { bits.push_u1(1); - bits.push_u5(self.chain_depth); + bits.push_u9(self.chain_depth); } // Encode the self type if known @@ -1154,13 +1153,17 @@ impl Context { } } - ctx.is_deferred = bits.read_bool(&mut idx); - ctx.is_return_landing = bits.read_bool(&mut idx); + if bits.read_bool(&mut idx) { + ctx.flags |= DEFERRED_FLAG; + } + if bits.read_bool(&mut idx) { + ctx.flags |= RETURN_LANDING_FLAG; + } if bits.read_u1(&mut idx) == 0 { - ctx.chain_depth = bits.read_u1(&mut idx) + ctx.chain_depth = bits.read_u1(&mut idx).into() } else { - ctx.chain_depth = bits.read_u5(&mut idx) + ctx.chain_depth = bits.read_u9(&mut idx); } loop { @@ -2577,13 +2580,13 @@ impl Context { self.reg_mapping = reg_mapping; } - pub fn get_chain_depth(&self) -> u8 { + pub fn get_chain_depth(&self) -> u16 { self.chain_depth } pub fn reset_chain_depth_and_defer(&mut self) { self.chain_depth = 0; - self.is_deferred = false; + self.flags &= !DEFERRED_FLAG; } pub fn increment_chain_depth(&mut self) { @@ -2594,23 +2597,23 @@ impl Context { } pub fn set_as_return_landing(&mut self) { - self.is_return_landing = true; + self.flags |= RETURN_LANDING_FLAG; } pub fn clear_return_landing(&mut self) { - self.is_return_landing = false; + self.flags &= !RETURN_LANDING_FLAG; } pub fn is_return_landing(&self) -> bool { - self.is_return_landing + self.flags & RETURN_LANDING_FLAG != 0 } pub fn mark_as_deferred(&mut self) { - self.is_deferred = true; + self.flags |= DEFERRED_FLAG; } pub fn is_deferred(&self) -> bool { - self.is_deferred + self.flags & DEFERRED_FLAG != 0 } /// Get an operand for the adjusted stack pointer address @@ -4373,6 +4376,7 @@ mod tests { // Check that we can store types in 4 bits, // and all local types in 32 bits assert_eq!(mem::size_of::(), 1); + assert_eq!(mem::size_of::(), 56); assert!(Type::BlockParamProxy as usize <= 0b1111); assert!(MAX_CTX_LOCALS * 4 <= 32); } diff --git a/yjit/src/log.rs b/yjit/src/log.rs index c5a724f7e1df52..223b76176e2aae 100644 --- a/yjit/src/log.rs +++ b/yjit/src/log.rs @@ -46,7 +46,7 @@ impl Log { } } - pub fn add_block_with_chain_depth(block_id: BlockId, chain_depth: u8) { + pub fn add_block_with_chain_depth(block_id: BlockId, chain_depth: u16) { if !Self::has_instance() { return; } From 5552e2c57f874d4bc19c41e6ad97e341ab46c8a6 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Mon, 24 Aug 2026 13:27:37 -0400 Subject: [PATCH 5/7] Shrink iseq->body struct: allocate iseq->body->variable lazily Most of the time in a production app most iseqs won't have iseq->body->variable set to anything non-default. We can allocate it lazily and save 32 bytes per iseq. Some large apps have millions of iseqs, so it adds up. 32B saved per iseq * 2mil iseqs =~ 64 MB --- ast.c | 2 +- compile.c | 29 ++++++++----- iseq.c | 77 ++++++++++++++++++++++++++++------ iseq.h | 40 ++++++++++-------- vm_backtrace.c | 4 +- vm_core.h | 18 ++++---- zjit/src/cruby_bindings.inc.rs | 18 ++++---- 7 files changed, 129 insertions(+), 59 deletions(-) diff --git a/ast.c b/ast.c index 721302c0304370..3f5c9a4ba6bd85 100644 --- a/ast.c +++ b/ast.c @@ -359,7 +359,7 @@ ast_s_of(rb_execution_context_t *ec, VALUE module, VALUE body, VALUE keep_script rb_raise(rb_eRuntimeError, "cannot get AST for ISEQ compiled by prism"); } - lines = ISEQ_BODY(iseq)->variable.script_lines; + lines = ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->script_lines : Qnil; VALUE path = rb_iseq_path(iseq); int e_option = RSTRING_LEN(path) == 2 && memcmp(RSTRING_PTR(path), "-e", 2) == 0; diff --git a/compile.c b/compile.c index 73a1065141219b..375fc4fe49fe17 100644 --- a/compile.c +++ b/compile.c @@ -1015,7 +1015,8 @@ rb_iseq_translate_threaded_code(rb_iseq_t *iseq) VALUE * rb_iseq_original_iseq(const rb_iseq_t *iseq) /* cold path */ { - VALUE *original_code = RUBY_ATOMIC_PTR_LOAD(ISEQ_BODY(iseq)->variable.original_iseq); + struct rb_iseq_variable *v = ISEQ_BODY(iseq)->variable; + VALUE *original_code = v ? RUBY_ATOMIC_PTR_LOAD(v->original_iseq) : NULL; if (original_code) return original_code; original_code = ALLOC_N(VALUE, ISEQ_BODY(iseq)->iseq_size); @@ -1037,7 +1038,8 @@ rb_iseq_original_iseq(const rb_iseq_t *iseq) /* cold path */ /* Concurrent callers can each build a copy; publish only fully * translated code and keep the first one. */ - VALUE *prev = ATOMIC_PTR_CAS(ISEQ_BODY(iseq)->variable.original_iseq, + v = rb_iseq_variable_ensure((rb_iseq_t *)iseq); + VALUE *prev = ATOMIC_PTR_CAS(v->original_iseq, NULL, original_code); if (prev) { SIZED_FREE_N(original_code, ISEQ_BODY(iseq)->iseq_size); @@ -1530,7 +1532,7 @@ new_child_iseq(rb_iseq_t *iseq, const NODE *const node, line_no, parent, isolated_depth ? isolated_depth + 1 : 0, type, ISEQ_COMPILE_DATA(iseq)->option, - ISEQ_BODY(iseq)->variable.script_lines); + ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->script_lines : Qnil); debugs("[new_child_iseq]< ---------------------------------------\n"); return ret_iseq; } @@ -9445,7 +9447,7 @@ compile_builtin_mandatory_only_method(rb_iseq_t *iseq, const NODE *node, const N rb_iseq_path(iseq), rb_iseq_realpath(iseq), nd_line(line_node), NULL, 0, ISEQ_TYPE_METHOD, ISEQ_COMPILE_DATA(iseq)->option, - ISEQ_BODY(iseq)->variable.script_lines); + ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->script_lines : Qnil); RB_OBJ_WRITE(iseq, &ISEQ_BODY(iseq)->mandatory_only_iseq, (VALUE)mandatory_only_iseq); ALLOCV_END(idtmp); @@ -13818,7 +13820,7 @@ ibf_dump_iseq_each(struct ibf_dump *dump, const rb_iseq_t *iseq) ibf_dump_write_small_value(dump, mandatory_only_iseq_index); ibf_dump_write_small_value(dump, IBF_BODY_OFFSET(ci_entries_offset)); ibf_dump_write_small_value(dump, IBF_BODY_OFFSET(outer_variables_offset)); - ibf_dump_write_small_value(dump, body->variable.flip_count); + ibf_dump_write_small_value(dump, body->variable ? body->variable->flip_count : 0); ibf_dump_write_small_value(dump, body->local_table_size); ibf_dump_write_small_value(dump, body->ivc_size); ibf_dump_write_small_value(dump, body->icvarc_size); @@ -14010,10 +14012,12 @@ ibf_load_iseq_each(struct ibf_load *load, rb_iseq_t *iseq, ibf_offset_t offset) load_body->ci_size = ci_size; load_body->insns_info.size = insns_info_size; - ISEQ_COVERAGE_SET(iseq, Qnil); + // variable is NULL from ZALLOC; only allocate if flip_count is non-zero. ISEQ_ORIGINAL_ISEQ_CLEAR(iseq); - load_body->variable.flip_count = variable_flip_count; - load_body->variable.script_lines = Qnil; + if (variable_flip_count) { + struct rb_iseq_variable *v = rb_iseq_variable_ensure(iseq); + v->flip_count = variable_flip_count; + } load_body->location.first_lineno = location_first_lineno; load_body->location.node_id = location_node_id; @@ -15238,8 +15242,13 @@ rb_iseq_dup_with_independent_caches(const rb_iseq_t *src_root) struct rb_iseq_constant_body *cb = ISEQ_BODY(copy); if (!cb->local_iseq) RB_OBJ_WRITE(copy, &cb->local_iseq, sb->local_iseq); RB_OBJ_WRITE(copy, &cb->location.pathobj, sb->location.pathobj); - RB_OBJ_WRITE(copy, &cb->variable.script_lines, sb->variable.script_lines); - ISEQ_COVERAGE_SET(copy, ISEQ_COVERAGE(src_root)); + VALUE sl = sb->variable ? sb->variable->script_lines : Qnil; + VALUE cov = ISEQ_COVERAGE(src_root); + if (!NIL_P(sl) || !NIL_P(cov)) { + struct rb_iseq_variable *v = rb_iseq_variable_ensure(copy); + RB_OBJ_WRITE(copy, &v->script_lines, sl); + RB_OBJ_WRITE(copy, &v->coverage, cov); + } if (i == 0) { RB_OBJ_WRITE(copy, &cb->parent_iseq, sb->parent_iseq); diff --git a/iseq.c b/iseq.c index 27bad3e7d09e0d..ac917032a90bdd 100644 --- a/iseq.c +++ b/iseq.c @@ -219,6 +219,7 @@ rb_iseq_free(const rb_iseq_t *iseq) } ISEQ_ORIGINAL_ISEQ_CLEAR(iseq); + if (body->variable) xfree(body->variable); struct rb_iseq_param_keyword *pkw = (struct rb_iseq_param_keyword *)body->param.keyword; if (pkw != NULL) { @@ -242,6 +243,51 @@ rb_iseq_free(const rb_iseq_t *iseq) RUBY_FREE_LEAVE("iseq"); } +/* CAS-publish the out-of-lined variable struct: concurrent callers (e.g. two + * dumpers hitting rb_iseq_original_iseq on a shared iseq) can each build a + * copy, mirroring the original_iseq CAS in compile.c. */ +struct rb_iseq_variable * +rb_iseq_variable_ensure(rb_iseq_t *iseq) +{ + struct rb_iseq_variable *v = ISEQ_BODY(iseq)->variable; + if (v) return v; + v = ALLOC(struct rb_iseq_variable); + v->flip_count = 0; + v->script_lines = Qnil; + v->coverage = Qnil; + v->pc2branchindex = Qnil; + v->original_iseq = NULL; + struct rb_iseq_variable *prev = ATOMIC_PTR_CAS(ISEQ_BODY(iseq)->variable, NULL, v); + if (prev) { + xfree(v); + v = prev; + } + return v; +} + +void +rb_iseq_coverage_set(rb_iseq_t *iseq, VALUE cov) +{ + struct rb_iseq_variable *v = rb_iseq_variable_ensure(iseq); + RB_OBJ_WRITE(iseq, &v->coverage, cov); +} + +void +rb_iseq_pc2branchindex_set(rb_iseq_t *iseq, VALUE h) +{ + struct rb_iseq_variable *v = rb_iseq_variable_ensure(iseq); + RB_OBJ_WRITE(iseq, &v->pc2branchindex, h); +} + +rb_snum_t +rb_iseq_flip_cnt_increment(const rb_iseq_t *iseq) +{ + struct rb_iseq_variable *v = rb_iseq_variable_ensure((rb_iseq_t *)iseq); + rb_snum_t cnt = v->flip_count; + v->flip_count += 1; + return cnt; +} + typedef VALUE iseq_value_itr_t(void *ctx, VALUE obj); static inline void @@ -377,7 +423,7 @@ rb_iseq_mark_and_move(rb_iseq_t *iseq, bool reference_updating) rb_iseq_mark_and_move_each_body_value(iseq, reference_updating ? ISEQ_ORIGINAL_ISEQ(iseq) : NULL); - rb_gc_mark_and_move(&body->variable.script_lines); + if (body->variable) rb_gc_mark_and_move(&body->variable->script_lines); rb_gc_mark_and_move(&body->location.label); rb_gc_mark_and_move(&body->location.base_label); rb_gc_mark_and_move(&body->location.pathobj); @@ -489,8 +535,10 @@ rb_iseq_mark_and_move(rb_iseq_t *iseq, bool reference_updating) // TODO: ractor aware coverage if (!rb_gc_checking_shareable()) { - rb_gc_mark_and_move(&body->variable.coverage); - rb_gc_mark_and_move(&body->variable.pc2branchindex); + if (body->variable) { + rb_gc_mark_and_move(&body->variable->coverage); + rb_gc_mark_and_move(&body->variable->pc2branchindex); + } } } @@ -542,6 +590,7 @@ rb_iseq_memsize(const rb_iseq_t *iseq) if (ISEQ_EXECUTABLE_P(iseq) && body) { size += sizeof(struct rb_iseq_constant_body); + if (body->variable) size += sizeof(struct rb_iseq_variable); size += body->iseq_size * sizeof(VALUE); size += body->insns_info.size * (sizeof(struct iseq_insn_info_entry) + sizeof(unsigned int)); size += body->local_table_size * sizeof(ID); // body->local_table @@ -759,15 +808,19 @@ prepare_iseq_build(rb_iseq_t *iseq, if (iseq != body->local_iseq) { RB_OBJ_WRITE(iseq, &body->location.base_label, ISEQ_BODY(body->local_iseq)->location.label); } - ISEQ_COVERAGE_SET(iseq, Qnil); + // variable is NULL from ZALLOC; accessors return the correct defaults + // (coverage=Qnil, flip_count=0, script_lines=Qnil, original_iseq=NULL). + // Only reset fields on an already-allocated variable (re-compilation). ISEQ_ORIGINAL_ISEQ_CLEAR(iseq); - body->variable.flip_count = 0; - - if (NIL_P(script_lines)) { - RB_OBJ_WRITE(iseq, &body->variable.script_lines, Qnil); + if (body->variable) { + body->variable->flip_count = 0; + RB_OBJ_WRITE(iseq, &body->variable->script_lines, Qnil); + RB_OBJ_WRITE(iseq, &body->variable->coverage, Qnil); } - else { - RB_OBJ_WRITE(iseq, &body->variable.script_lines, rb_ractor_make_shareable(script_lines)); + + if (!NIL_P(script_lines)) { + struct rb_iseq_variable *v = rb_iseq_variable_ensure(iseq); + RB_OBJ_WRITE(iseq, &v->script_lines, rb_ractor_make_shareable(script_lines)); } ISEQ_COMPILE_DATA_ALLOC(iseq); @@ -1130,7 +1183,7 @@ rb_iseq_new_with_opt(VALUE ast_value, VALUE name, VALUE path, VALUE realpath, script_lines = rb_parser_build_script_lines_from(body->script_lines); } else if (parent) { - script_lines = ISEQ_BODY(parent)->variable.script_lines; + script_lines = ISEQ_BODY(parent)->variable ? ISEQ_BODY(parent)->variable->script_lines : Qnil; } prepare_iseq_build(iseq, name, path, realpath, first_lineno, node ? &node->nd_loc : NULL, prepare_node_id(node), @@ -4596,7 +4649,7 @@ static VALUE iseqw_script_lines(VALUE self) { const rb_iseq_t *iseq = iseqw_check(self); - return ISEQ_BODY(iseq)->variable.script_lines; + return ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->script_lines : Qnil; } /* Returns the hash of the source this iseq was compiled from, or nil if it diff --git a/iseq.h b/iseq.h index e641dd6aed1f5e..9d9679f09b3010 100644 --- a/iseq.h +++ b/iseq.h @@ -58,41 +58,45 @@ typedef void (*rb_iseq_callback)(const rb_iseq_t *, void *); extern const ID rb_iseq_shared_exc_local_tbl[]; -#define ISEQ_COVERAGE(iseq) ISEQ_BODY(iseq)->variable.coverage -#define ISEQ_COVERAGE_SET(iseq, cov) RB_OBJ_WRITE(iseq, &ISEQ_BODY(iseq)->variable.coverage, cov) +/* Ensure body->variable is allocated, returning the struct. */ +struct rb_iseq_variable *rb_iseq_variable_ensure(rb_iseq_t *iseq); + +#define ISEQ_COVERAGE(iseq) \ + (ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->coverage : Qnil) +void rb_iseq_coverage_set(rb_iseq_t *iseq, VALUE cov); +#define ISEQ_COVERAGE_SET(iseq, cov) rb_iseq_coverage_set(iseq, cov) #define ISEQ_LINE_COVERAGE(iseq) RARRAY_AREF(ISEQ_COVERAGE(iseq), COVERAGE_INDEX_LINES) #define ISEQ_BRANCH_COVERAGE(iseq) RARRAY_AREF(ISEQ_COVERAGE(iseq), COVERAGE_INDEX_BRANCHES) -#define ISEQ_PC2BRANCHINDEX(iseq) ISEQ_BODY(iseq)->variable.pc2branchindex -#define ISEQ_PC2BRANCHINDEX_SET(iseq, h) RB_OBJ_WRITE(iseq, &ISEQ_BODY(iseq)->variable.pc2branchindex, h) +#define ISEQ_PC2BRANCHINDEX(iseq) \ + (ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->pc2branchindex : Qnil) +void rb_iseq_pc2branchindex_set(rb_iseq_t *iseq, VALUE h); +#define ISEQ_PC2BRANCHINDEX_SET(iseq, h) rb_iseq_pc2branchindex_set(iseq, h) -#define ISEQ_FLIP_CNT(iseq) ISEQ_BODY(iseq)->variable.flip_count +#define ISEQ_FLIP_CNT(iseq) \ + (ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->flip_count : 0) +rb_snum_t rb_iseq_flip_cnt_increment(const rb_iseq_t *iseq); +#define ISEQ_FLIP_CNT_INCREMENT(iseq) rb_iseq_flip_cnt_increment(iseq) #define ISEQ_FROZEN_STRING_LITERAL_ENABLED 1 #define ISEQ_FROZEN_STRING_LITERAL_DISABLED 0 #define ISEQ_FROZEN_STRING_LITERAL_UNSET -1 -static inline rb_snum_t -ISEQ_FLIP_CNT_INCREMENT(const rb_iseq_t *iseq) -{ - rb_snum_t cnt = ISEQ_BODY(iseq)->variable.flip_count; - ISEQ_BODY(iseq)->variable.flip_count += 1; - return cnt; -} - static inline VALUE * ISEQ_ORIGINAL_ISEQ(const rb_iseq_t *iseq) { - return ISEQ_BODY(iseq)->variable.original_iseq; + return ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->original_iseq : NULL; } static inline void ISEQ_ORIGINAL_ISEQ_CLEAR(const rb_iseq_t *iseq) { - VALUE *ptr = (VALUE *)ISEQ_BODY(iseq)->variable.original_iseq; - if (ptr) { - ISEQ_BODY(iseq)->variable.original_iseq = NULL; - SIZED_FREE_N(ptr, ISEQ_BODY(iseq)->iseq_size); + if (ISEQ_BODY(iseq)->variable) { + VALUE *ptr = ISEQ_BODY(iseq)->variable->original_iseq; + if (ptr) { + ISEQ_BODY(iseq)->variable->original_iseq = NULL; + SIZED_FREE_N(ptr, ISEQ_BODY(iseq)->iseq_size); + } } } diff --git a/vm_backtrace.c b/vm_backtrace.c index 54845124f100d8..b2fbbf8b21d172 100644 --- a/vm_backtrace.c +++ b/vm_backtrace.c @@ -560,7 +560,7 @@ location_source_first_lineno(const rb_iseq_t *iseq, VALUE script_lines) while (ISEQ_BODY(source_iseq)->parent_iseq) { const rb_iseq_t *parent = ISEQ_BODY(source_iseq)->parent_iseq; - if (ISEQ_BODY(parent)->variable.script_lines != script_lines) break; + if ((ISEQ_BODY(parent)->variable ? ISEQ_BODY(parent)->variable->script_lines : Qnil) != script_lines) break; source_iseq = parent; } @@ -608,7 +608,7 @@ location_source_range_m(VALUE self) VALUE path = rb_iseq_path(iseq); VALUE absolute_path = rb_iseq_realpath(iseq); - VALUE script_lines = ISEQ_BODY(iseq)->variable.script_lines; + VALUE script_lines = ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->script_lines : Qnil; VALUE source; VALUE parser_path = path; int first_lineno = 1; diff --git a/vm_core.h b/vm_core.h index 6b86c9b3e81c12..5f316b90e147ed 100644 --- a/vm_core.h +++ b/vm_core.h @@ -416,6 +416,16 @@ enum lvar_state { lvar_reassigned, }; +/* Lazily-allocated per-iseq variable data. NULL when unused (the common case: + * no coverage, no script_lines, no flip-flops, no disassembly). */ +struct rb_iseq_variable { + rb_snum_t flip_count; + VALUE script_lines; + VALUE coverage; + VALUE pc2branchindex; + VALUE *original_iseq; +}; + struct rb_iseq_constant_body { enum rb_iseq_type type; @@ -527,13 +537,7 @@ struct rb_iseq_constant_body { union iseq_inline_storage_entry *is_entries; /* [ TS_IVC | TS_ICVARC | TS_ISE | TS_IC ] */ struct rb_call_data *call_data; //struct rb_call_data calls[ci_size]; - struct { - rb_snum_t flip_count; - VALUE script_lines; - VALUE coverage; - VALUE pc2branchindex; - VALUE *original_iseq; - } variable; + struct rb_iseq_variable *variable; unsigned int local_table_size; unsigned int ic_size; // Number of IC caches diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index ebd8359e798c1c..2418457bfbfe9b 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -650,6 +650,14 @@ pub type rb_jit_func_t = ::std::option::Option< ) -> VALUE, >; #[repr(C)] +pub struct rb_iseq_variable { + pub flip_count: rb_snum_t, + pub script_lines: VALUE, + pub coverage: VALUE, + pub pc2branchindex: VALUE, + pub original_iseq: *mut VALUE, +} +#[repr(C)] #[derive(Debug, Copy, Clone)] pub struct rb_iseq_constant_body_rb_iseq_parameters { pub flags: rb_iseq_constant_body_rb_iseq_parameters__bindgen_ty_1, @@ -1273,16 +1281,8 @@ pub union rb_iseq_constant_body_iseq_insn_info__bindgen_ty_1 { pub succ_index_table: *mut succ_index_table, } #[repr(C)] -pub struct rb_iseq_constant_body__bindgen_ty_1 { - pub flip_count: rb_snum_t, - pub script_lines: VALUE, - pub coverage: VALUE, - pub pc2branchindex: VALUE, - pub original_iseq: *mut VALUE, -} -#[repr(C)] #[derive(Copy, Clone)] -pub union rb_iseq_constant_body__bindgen_ty_2 { +pub union rb_iseq_constant_body__bindgen_ty_1 { pub list: *mut iseq_bits_t, pub single: iseq_bits_t, } From 6e3061c7d3a48117f4b1d0de59abef8d4f1777d4 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Fri, 14 Aug 2026 15:08:52 -0400 Subject: [PATCH 6/7] Add accessors for ISEQ_BODY(iseq)->variable fields --- ast.c | 2 +- compile.c | 10 ++--- iseq.c | 28 ++++++------ iseq.h | 80 +++++++++++++++++++++++++--------- vm_backtrace.c | 4 +- zjit/src/cruby_bindings.inc.rs | 2 +- 6 files changed, 84 insertions(+), 42 deletions(-) diff --git a/ast.c b/ast.c index 3f5c9a4ba6bd85..1f08ee2a48b208 100644 --- a/ast.c +++ b/ast.c @@ -359,7 +359,7 @@ ast_s_of(rb_execution_context_t *ec, VALUE module, VALUE body, VALUE keep_script rb_raise(rb_eRuntimeError, "cannot get AST for ISEQ compiled by prism"); } - lines = ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->script_lines : Qnil; + lines = ISEQ_SCRIPT_LINES(iseq); VALUE path = rb_iseq_path(iseq); int e_option = RSTRING_LEN(path) == 2 && memcmp(RSTRING_PTR(path), "-e", 2) == 0; diff --git a/compile.c b/compile.c index 375fc4fe49fe17..7fd5f8e33d2d38 100644 --- a/compile.c +++ b/compile.c @@ -1015,7 +1015,7 @@ rb_iseq_translate_threaded_code(rb_iseq_t *iseq) VALUE * rb_iseq_original_iseq(const rb_iseq_t *iseq) /* cold path */ { - struct rb_iseq_variable *v = ISEQ_BODY(iseq)->variable; + struct rb_iseq_variable *v = ISEQ_VARIABLE(iseq); VALUE *original_code = v ? RUBY_ATOMIC_PTR_LOAD(v->original_iseq) : NULL; if (original_code) return original_code; @@ -1532,7 +1532,7 @@ new_child_iseq(rb_iseq_t *iseq, const NODE *const node, line_no, parent, isolated_depth ? isolated_depth + 1 : 0, type, ISEQ_COMPILE_DATA(iseq)->option, - ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->script_lines : Qnil); + ISEQ_SCRIPT_LINES(iseq)); debugs("[new_child_iseq]< ---------------------------------------\n"); return ret_iseq; } @@ -9447,7 +9447,7 @@ compile_builtin_mandatory_only_method(rb_iseq_t *iseq, const NODE *node, const N rb_iseq_path(iseq), rb_iseq_realpath(iseq), nd_line(line_node), NULL, 0, ISEQ_TYPE_METHOD, ISEQ_COMPILE_DATA(iseq)->option, - ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->script_lines : Qnil); + ISEQ_SCRIPT_LINES(iseq)); RB_OBJ_WRITE(iseq, &ISEQ_BODY(iseq)->mandatory_only_iseq, (VALUE)mandatory_only_iseq); ALLOCV_END(idtmp); @@ -13820,7 +13820,7 @@ ibf_dump_iseq_each(struct ibf_dump *dump, const rb_iseq_t *iseq) ibf_dump_write_small_value(dump, mandatory_only_iseq_index); ibf_dump_write_small_value(dump, IBF_BODY_OFFSET(ci_entries_offset)); ibf_dump_write_small_value(dump, IBF_BODY_OFFSET(outer_variables_offset)); - ibf_dump_write_small_value(dump, body->variable ? body->variable->flip_count : 0); + ibf_dump_write_small_value(dump, ISEQ_FLIP_CNT(iseq)); ibf_dump_write_small_value(dump, body->local_table_size); ibf_dump_write_small_value(dump, body->ivc_size); ibf_dump_write_small_value(dump, body->icvarc_size); @@ -15242,7 +15242,7 @@ rb_iseq_dup_with_independent_caches(const rb_iseq_t *src_root) struct rb_iseq_constant_body *cb = ISEQ_BODY(copy); if (!cb->local_iseq) RB_OBJ_WRITE(copy, &cb->local_iseq, sb->local_iseq); RB_OBJ_WRITE(copy, &cb->location.pathobj, sb->location.pathobj); - VALUE sl = sb->variable ? sb->variable->script_lines : Qnil; + VALUE sl = ISEQ_SCRIPT_LINES(src_root); VALUE cov = ISEQ_COVERAGE(src_root); if (!NIL_P(sl) || !NIL_P(cov)) { struct rb_iseq_variable *v = rb_iseq_variable_ensure(copy); diff --git a/iseq.c b/iseq.c index ac917032a90bdd..c1f8bc6fb66d8b 100644 --- a/iseq.c +++ b/iseq.c @@ -249,13 +249,13 @@ rb_iseq_free(const rb_iseq_t *iseq) struct rb_iseq_variable * rb_iseq_variable_ensure(rb_iseq_t *iseq) { - struct rb_iseq_variable *v = ISEQ_BODY(iseq)->variable; + struct rb_iseq_variable *v = ISEQ_VARIABLE(iseq); if (v) return v; v = ALLOC(struct rb_iseq_variable); v->flip_count = 0; v->script_lines = Qnil; - v->coverage = Qnil; - v->pc2branchindex = Qnil; + v->coverage = Qfalse; + v->pc2branchindex = Qfalse; v->original_iseq = NULL; struct rb_iseq_variable *prev = ATOMIC_PTR_CAS(ISEQ_BODY(iseq)->variable, NULL, v); if (prev) { @@ -268,6 +268,7 @@ rb_iseq_variable_ensure(rb_iseq_t *iseq) void rb_iseq_coverage_set(rb_iseq_t *iseq, VALUE cov) { + if (!RTEST(cov) && !ISEQ_VARIABLE(iseq)) return; struct rb_iseq_variable *v = rb_iseq_variable_ensure(iseq); RB_OBJ_WRITE(iseq, &v->coverage, cov); } @@ -275,6 +276,7 @@ rb_iseq_coverage_set(rb_iseq_t *iseq, VALUE cov) void rb_iseq_pc2branchindex_set(rb_iseq_t *iseq, VALUE h) { + if (!RTEST(h) && !ISEQ_VARIABLE(iseq)) return; struct rb_iseq_variable *v = rb_iseq_variable_ensure(iseq); RB_OBJ_WRITE(iseq, &v->pc2branchindex, h); } @@ -423,7 +425,8 @@ rb_iseq_mark_and_move(rb_iseq_t *iseq, bool reference_updating) rb_iseq_mark_and_move_each_body_value(iseq, reference_updating ? ISEQ_ORIGINAL_ISEQ(iseq) : NULL); - if (body->variable) rb_gc_mark_and_move(&body->variable->script_lines); + struct rb_iseq_variable *v = ISEQ_VARIABLE(iseq); + if (v) rb_gc_mark_and_move(&v->script_lines); rb_gc_mark_and_move(&body->location.label); rb_gc_mark_and_move(&body->location.base_label); rb_gc_mark_and_move(&body->location.pathobj); @@ -535,9 +538,9 @@ rb_iseq_mark_and_move(rb_iseq_t *iseq, bool reference_updating) // TODO: ractor aware coverage if (!rb_gc_checking_shareable()) { - if (body->variable) { - rb_gc_mark_and_move(&body->variable->coverage); - rb_gc_mark_and_move(&body->variable->pc2branchindex); + if (v) { + rb_gc_mark_and_move(&v->coverage); + rb_gc_mark_and_move(&v->pc2branchindex); } } } @@ -808,9 +811,6 @@ prepare_iseq_build(rb_iseq_t *iseq, if (iseq != body->local_iseq) { RB_OBJ_WRITE(iseq, &body->location.base_label, ISEQ_BODY(body->local_iseq)->location.label); } - // variable is NULL from ZALLOC; accessors return the correct defaults - // (coverage=Qnil, flip_count=0, script_lines=Qnil, original_iseq=NULL). - // Only reset fields on an already-allocated variable (re-compilation). ISEQ_ORIGINAL_ISEQ_CLEAR(iseq); if (body->variable) { body->variable->flip_count = 0; @@ -1183,7 +1183,7 @@ rb_iseq_new_with_opt(VALUE ast_value, VALUE name, VALUE path, VALUE realpath, script_lines = rb_parser_build_script_lines_from(body->script_lines); } else if (parent) { - script_lines = ISEQ_BODY(parent)->variable ? ISEQ_BODY(parent)->variable->script_lines : Qnil; + script_lines = ISEQ_SCRIPT_LINES(parent); } prepare_iseq_build(iseq, name, path, realpath, first_lineno, node ? &node->nd_loc : NULL, prepare_node_id(node), @@ -1680,7 +1680,9 @@ remove_coverage_i(void *vstart, void *vend, size_t stride, void *data) if (rb_obj_is_iseq(v)) { rb_iseq_t *iseq = (rb_iseq_t *)v; - ISEQ_COVERAGE_SET(iseq, Qnil); + if (ISEQ_VARIABLE(iseq)) { + ISEQ_COVERAGE_SET(iseq, Qnil); + } } asan_poison_object_if(ptr, v); @@ -4649,7 +4651,7 @@ static VALUE iseqw_script_lines(VALUE self) { const rb_iseq_t *iseq = iseqw_check(self); - return ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->script_lines : Qnil; + return ISEQ_SCRIPT_LINES(iseq); } /* Returns the hash of the source this iseq was compiled from, or nil if it diff --git a/iseq.h b/iseq.h index 9d9679f09b3010..e75e16788f4ab3 100644 --- a/iseq.h +++ b/iseq.h @@ -61,40 +61,80 @@ extern const ID rb_iseq_shared_exc_local_tbl[]; /* Ensure body->variable is allocated, returning the struct. */ struct rb_iseq_variable *rb_iseq_variable_ensure(rb_iseq_t *iseq); -#define ISEQ_COVERAGE(iseq) \ - (ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->coverage : Qnil) +static inline struct rb_iseq_variable * +ISEQ_BODY_VARIABLE(const rb_iseq_t *iseq) +{ + return ISEQ_BODY(iseq)->variable; +} + +/* NULL-safe read accessors for body->variable fields. */ +static inline VALUE +ISEQ_BODY_VARIABLE_SCRIPT_LINES(const rb_iseq_t *iseq) +{ + struct rb_iseq_variable *v = ISEQ_BODY_VARIABLE(iseq); + return v ? v->script_lines : Qnil; +} + +static inline VALUE +ISEQ_BODY_VARIABLE_COVERAGE(const rb_iseq_t *iseq) +{ + struct rb_iseq_variable *v = ISEQ_BODY_VARIABLE(iseq); + return v ? v->coverage : Qfalse; +} + +static inline VALUE +ISEQ_BODY_VARIABLE_PC2BRANCHINDEX(const rb_iseq_t *iseq) +{ + struct rb_iseq_variable *v = ISEQ_BODY_VARIABLE(iseq); + return v ? v->pc2branchindex : Qfalse; +} + +static inline rb_snum_t +ISEQ_BODY_VARIABLE_FLIP_CNT(const rb_iseq_t *iseq) +{ + struct rb_iseq_variable *v = ISEQ_BODY_VARIABLE(iseq); + return v ? v->flip_count : 0; +} + +static inline VALUE * +ISEQ_BODY_VARIABLE_ORIGINAL_ISEQ(const rb_iseq_t *iseq) +{ + struct rb_iseq_variable *v = ISEQ_BODY_VARIABLE(iseq); + return v ? v->original_iseq : NULL; +} + +/* Write accessors (lazily allocate variable as needed). */ void rb_iseq_coverage_set(rb_iseq_t *iseq, VALUE cov); -#define ISEQ_COVERAGE_SET(iseq, cov) rb_iseq_coverage_set(iseq, cov) +void rb_iseq_pc2branchindex_set(rb_iseq_t *iseq, VALUE h); +rb_snum_t rb_iseq_flip_cnt_increment(const rb_iseq_t *iseq); + +/* Short macros for reading variable fields. */ +#define ISEQ_VARIABLE(iseq) ISEQ_BODY_VARIABLE(iseq) +#define ISEQ_SCRIPT_LINES(iseq) ISEQ_BODY_VARIABLE_SCRIPT_LINES(iseq) +#define ISEQ_COVERAGE(iseq) ISEQ_BODY_VARIABLE_COVERAGE(iseq) +#define ISEQ_COVERAGE_SET(iseq, cov) rb_iseq_coverage_set(iseq, cov) #define ISEQ_LINE_COVERAGE(iseq) RARRAY_AREF(ISEQ_COVERAGE(iseq), COVERAGE_INDEX_LINES) #define ISEQ_BRANCH_COVERAGE(iseq) RARRAY_AREF(ISEQ_COVERAGE(iseq), COVERAGE_INDEX_BRANCHES) -#define ISEQ_PC2BRANCHINDEX(iseq) \ - (ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->pc2branchindex : Qnil) -void rb_iseq_pc2branchindex_set(rb_iseq_t *iseq, VALUE h); -#define ISEQ_PC2BRANCHINDEX_SET(iseq, h) rb_iseq_pc2branchindex_set(iseq, h) +#define ISEQ_PC2BRANCHINDEX(iseq) ISEQ_BODY_VARIABLE_PC2BRANCHINDEX(iseq) +#define ISEQ_PC2BRANCHINDEX_SET(iseq,h) rb_iseq_pc2branchindex_set(iseq, h) -#define ISEQ_FLIP_CNT(iseq) \ - (ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->flip_count : 0) -rb_snum_t rb_iseq_flip_cnt_increment(const rb_iseq_t *iseq); -#define ISEQ_FLIP_CNT_INCREMENT(iseq) rb_iseq_flip_cnt_increment(iseq) +#define ISEQ_FLIP_CNT(iseq) ISEQ_BODY_VARIABLE_FLIP_CNT(iseq) +#define ISEQ_FLIP_CNT_INCREMENT(iseq) rb_iseq_flip_cnt_increment(iseq) +#define ISEQ_ORIGINAL_ISEQ(iseq) ISEQ_BODY_VARIABLE_ORIGINAL_ISEQ(iseq) #define ISEQ_FROZEN_STRING_LITERAL_ENABLED 1 #define ISEQ_FROZEN_STRING_LITERAL_DISABLED 0 #define ISEQ_FROZEN_STRING_LITERAL_UNSET -1 -static inline VALUE * -ISEQ_ORIGINAL_ISEQ(const rb_iseq_t *iseq) -{ - return ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->original_iseq : NULL; -} - static inline void ISEQ_ORIGINAL_ISEQ_CLEAR(const rb_iseq_t *iseq) { - if (ISEQ_BODY(iseq)->variable) { - VALUE *ptr = ISEQ_BODY(iseq)->variable->original_iseq; + struct rb_iseq_variable *v = ISEQ_BODY_VARIABLE(iseq); + if (v) { + VALUE *ptr = v->original_iseq; if (ptr) { - ISEQ_BODY(iseq)->variable->original_iseq = NULL; + v->original_iseq = NULL; SIZED_FREE_N(ptr, ISEQ_BODY(iseq)->iseq_size); } } diff --git a/vm_backtrace.c b/vm_backtrace.c index b2fbbf8b21d172..f13d4273179785 100644 --- a/vm_backtrace.c +++ b/vm_backtrace.c @@ -560,7 +560,7 @@ location_source_first_lineno(const rb_iseq_t *iseq, VALUE script_lines) while (ISEQ_BODY(source_iseq)->parent_iseq) { const rb_iseq_t *parent = ISEQ_BODY(source_iseq)->parent_iseq; - if ((ISEQ_BODY(parent)->variable ? ISEQ_BODY(parent)->variable->script_lines : Qnil) != script_lines) break; + if (ISEQ_SCRIPT_LINES(parent) != script_lines) break; source_iseq = parent; } @@ -608,7 +608,7 @@ location_source_range_m(VALUE self) VALUE path = rb_iseq_path(iseq); VALUE absolute_path = rb_iseq_realpath(iseq); - VALUE script_lines = ISEQ_BODY(iseq)->variable ? ISEQ_BODY(iseq)->variable->script_lines : Qnil; + VALUE script_lines = ISEQ_SCRIPT_LINES(iseq); VALUE source; VALUE parser_path = path; int first_lineno = 1; diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 2418457bfbfe9b..68ff278b6748db 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2099,7 +2099,7 @@ pub struct zjit_jit_frame { pub stack: __IncompleteArrayField, } pub const ISEQ_BODY_OFFSET_PARAM: zjit_struct_offsets = 16; -pub const ISEQ_BODY_OFFSET_OUTER_VARIABLES: zjit_struct_offsets = 280; +pub const ISEQ_BODY_OFFSET_OUTER_VARIABLES: zjit_struct_offsets = 248; pub const RUBY_OFFSET_THREAD_RACTOR: zjit_struct_offsets = 24; pub type zjit_struct_offsets = u32; #[repr(C)] From c4fdf07d44e0360c1f8d525066cdb00596c0f6cb Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 24 Aug 2026 14:44:40 -0400 Subject: [PATCH 7/7] ZJIT: Reduce SideExit HashMap rehash (#18411) If we don't pre-size the HashMap, it has to re-hash when resizing. Hashing a SideExit is slow. Give a size hint so we only hash ~once per exit. --- zjit/src/backend/lir.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 7c91d91691b072..2846576322c325 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -3228,7 +3228,7 @@ impl Assembler let exit_block = self.new_block_without_id("side_exits"); // Map from SideExit to compiled Label. This table is used to deduplicate side exit code. - let mut compiled_exits: HashMap = HashMap::new(); + let mut compiled_exits: HashMap = HashMap::with_capacity(targets.len()); // Start a new perf range for side exits let perf_symbol = if get_option!(perf) == Some(PerfMap::HIR) {