From 8993cb5a3c87dcad17904bd8a0b69e235baf03e9 Mon Sep 17 00:00:00 2001 From: Hiroya Fujinami Date: Tue, 6 Oct 2026 16:10:52 +0900 Subject: [PATCH] Make `remove_unreachable_chunk` linear in method size (#18823) * Make remove_unreachable_chunk linear in method size remove_unreachable_chunk allocated and cleared a label_no-sized counter array on every call, and iseq_peephole_optimize calls it for every unconditional jump and leave, so compiling a method cost O(jumps * labels): quadratic in the size of the method. Keep one scratch table in iseq_compile_data and stamp each entry with a generation, so a new call invalidates the table by bumping the generation instead of clearing it. The arrays live in the compile arena and grow by doubling; a stamp wrap-around falls back to one full clear. This also removes an unbounded ALLOCA_N sized by label_no. The compiled output is unchanged: the reproducer corpus compiles to byte-identical iseq binaries, and the reproducer drops from 3.6 s to 0.13 s. [Bug #22313] * Make unreachable_unref fields local struct https://github.com/ruby/ruby/pull/18823#issuecomment-5769711739 * Use ALLOCV_N instead of VLA * Add a test case * Remove the test case previously added --- compile.c | 67 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/compile.c b/compile.c index 49114a1bc83..1beba1c2b84 100644 --- a/compile.c +++ b/compile.c @@ -3242,16 +3242,43 @@ find_destination(INSN *i) return 0; } +struct unreachable_unref { + const unsigned int capacity; + unsigned int gen; + int *counts; + unsigned int *stamps; +}; + static int -remove_unreachable_chunk(rb_iseq_t *iseq, LINK_ELEMENT *i) +unreachable_unref_count(const struct unreachable_unref *unref, const LABEL *lab) +{ + unsigned int no = (unsigned int)lab->label_no; + return unref->stamps[no] == unref->gen ? unref->counts[no] : 0; +} + +static void +unreachable_unref_increment(const struct unreachable_unref *unref, const LABEL *lab) +{ + unsigned int no = (unsigned int)lab->label_no; + if (unref->stamps[no] != unref->gen) { + unref->stamps[no] = unref->gen; + unref->counts[no] = 0; + } + unref->counts[no]++; +} + +static int +remove_unreachable_chunk(rb_iseq_t *iseq, struct unreachable_unref *unref, LINK_ELEMENT *i) { LINK_ELEMENT *first = i, *end, *scan, *pending_end = 0; LABEL *pending = 0; - int *unref_counts = 0, nlabels = ISEQ_COMPILE_DATA(iseq)->label_no; if (!i) return 0; - unref_counts = ALLOCA_N(int, nlabels); - MEMZERO(unref_counts, int, nlabels); + if (++unref->gen == 0) { + /* The stamp wrapped around, so every stale stamp became ambiguous. */ + MEMZERO(unref->stamps, unsigned int, unref->capacity); + unref->gen = 1; + } end = i; scan = i; @@ -3264,8 +3291,8 @@ remove_unreachable_chunk(rb_iseq_t *iseq, LINK_ELEMENT *i) break; } else if ((lab = find_destination((INSN *)scan)) != 0) { - unref_counts[lab->label_no]++; - if (lab == pending && lab->refcnt <= unref_counts[lab->label_no]) { + unreachable_unref_increment(unref, lab); + if (lab == pending && lab->refcnt <= unreachable_unref_count(unref, lab)) { pending = 0; } } @@ -3276,7 +3303,7 @@ remove_unreachable_chunk(rb_iseq_t *iseq, LINK_ELEMENT *i) if (pending) break; return 0; } - if (lab->refcnt > unref_counts[lab->label_no]) { + if (lab->refcnt > unreachable_unref_count(unref, lab)) { if (pending) break; pending = lab; pending_end = (scan == first) ? 0 : end; @@ -3493,7 +3520,7 @@ iseq_reg_compile(rb_iseq_t *iseq, VALUE str, int options, const char *sourcefile } static int -iseq_peephole_optimize(rb_iseq_t *iseq, LINK_ELEMENT *list, const int do_tailcallopt) +iseq_peephole_optimize(rb_iseq_t *iseq, struct unreachable_unref *unref, LINK_ELEMENT *list, const int do_tailcallopt) { INSN *const iobj = (INSN *)list; @@ -3531,7 +3558,7 @@ iseq_peephole_optimize(rb_iseq_t *iseq, LINK_ELEMENT *list, const int do_tailcal * LABEL2 directly */ if (replace_destination(iobj, diobj)) { - remove_unreachable_chunk(iseq, iobj->link.next); + remove_unreachable_chunk(iseq, unref, iobj->link.next); goto again; } } @@ -3604,7 +3631,7 @@ iseq_peephole_optimize(rb_iseq_t *iseq, LINK_ELEMENT *list, const int do_tailcal ELEM_REPLACE(&piobj->link, &popiobj->link); } } - if (remove_unreachable_chunk(iseq, iobj->link.next)) { + if (remove_unreachable_chunk(iseq, unref, iobj->link.next)) { goto again; } } @@ -3640,7 +3667,7 @@ iseq_peephole_optimize(rb_iseq_t *iseq, LINK_ELEMENT *list, const int do_tailcal } if (IS_INSN_ID(iobj, leave)) { - remove_unreachable_chunk(iseq, iobj->link.next); + remove_unreachable_chunk(iseq, unref, iobj->link.next); } /* @@ -4097,7 +4124,7 @@ iseq_peephole_optimize(rb_iseq_t *iseq, LINK_ELEMENT *list, const int do_tailcal } label->refcnt++; ELEM_INSERT_NEXT(next, &label->link); - CHECK(iseq_peephole_optimize(iseq, get_next_insn(jump), do_tailcallopt)); + CHECK(iseq_peephole_optimize(iseq, unref, get_next_insn(jump), do_tailcallopt)); } else { ELEM_REMOVE(next); @@ -4588,6 +4615,17 @@ iseq_optimize(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) int do_block_optimization = 0; LABEL * block_loop_label = NULL; + int nlabels = ISEQ_COMPILE_DATA(iseq)->label_no; + VALUE unref_counts_buf = 0, unref_stamps_buf = 0; + struct unreachable_unref unreachable_unref = { + .capacity = nlabels, + .gen = 0, + .counts = ALLOCV_N(int, unref_counts_buf, nlabels), + .stamps = ALLOCV_N(unsigned int, unref_stamps_buf, nlabels), + }; + MEMZERO(unreachable_unref.counts, int, nlabels); + MEMZERO(unreachable_unref.stamps, unsigned int, nlabels); + // If we're optimizing a block if (ISEQ_BODY(iseq)->type == ISEQ_TYPE_BLOCK) { do_block_optimization = 1; @@ -4603,7 +4641,7 @@ iseq_optimize(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) while (list) { if (IS_INSN(list)) { if (do_peepholeopt) { - iseq_peephole_optimize(iseq, list, tailcallopt); + iseq_peephole_optimize(iseq, &unreachable_unref, list, tailcallopt); } if (do_si) { iseq_specialized_instruction(iseq, (INSN *)list); @@ -4673,6 +4711,9 @@ iseq_optimize(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) ELEM_REMOVE(le); } } + + ALLOCV_END(unref_counts_buf); + ALLOCV_END(unref_stamps_buf); return COMPILE_OK; }