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
This commit is contained in:
Hiroya Fujinami authored and GitHub committed 2026-10-06 16:10:52 +09:00
1 parent dc53a3e8fd
commit 8993cb5a3c
1 file changed
+54 -13
Notes: git 2026-10-06 07:11:21 +00:00
Merged-By: makenowjust <make.just.on@gmail.com>
+54 -13
View File
@@ -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;
}