From 3245f8e111feef6fbc77be34455be34b106d632e Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Thu, 15 Oct 2026 22:49:40 +0800 Subject: [PATCH 1/2] Drop trivial block parameters before GVN and LICM A value that enters a loop and is never reassigned there reached the loop header as a block parameter that the back edge passes to itself. SSA sealing removes such parameters, but it seals each block once in layout order: the header is sealed while its back edge still carries the loop body's own, not yet sealed, parameters, and only once the body is sealed do those turn out to be the header's parameter. Nothing looks at the header again. The one pass that dropped what was left ran inside strength reduction, after GVN and LICM had treated every use of the parameter as defined inside the loop, so LICM hoisted nothing from for (i = 0; i < n; i++) a[i] = i + x * y; and GVN could not match x * y in the loop with the same product computed before it. Parameter simplification is now a pass of its own, run after the first CFG simplification so that local CSE, GVN and LICM see the input itself. It leaves the entry block's parameters alone: they are the function's arguments, and a back edge into the entry block that passes one value does not make an argument redundant. Dominance is built only once some parameter has a single incoming value. A loop using x * y, y ^ z and x << 3 runs 26.5 percent fewer instructions on x86-64. Under QEMU its host instructions drop by 8 percent for Armv7, 12 percent for AArch64 and 74 percent for RV32. The native throughput fixtures compile to the same bytes. A stage 0 self-compile runs about 1 percent more instructions, and the stage 1 compiler it builds runs 0.2 percent fewer. Close #283 --- docs/vir.md | 17 ++++--- src/vir-frontend.c | 1 + src/vir.c | 72 ++++++++++++++++++++++------- src/vir.h | 8 ++++ tests/vir-core.c | 113 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 188 insertions(+), 23 deletions(-) diff --git a/docs/vir.md b/docs/vir.md index a5cb1887..f619d08e 100644 --- a/docs/vir.md +++ b/docs/vir.md @@ -256,17 +256,20 @@ elimination (DCE) removes unused work. 1. Remove unreachable blocks. 2. SCCP and CFG simplification. -3. Local CSE. -4. GVN. -5. Reassociation, LICM and pointer-induction strength reduction. -6. Local CSE and DCE, including ordinary overwritten-store elimination. -7. CFG simplification, then a final local CSE and DCE over the simplified graph. -8. Full graph verification before dumps and machine lowering. +3. Block-parameter simplification: a parameter that receives one value on + every edge, or itself on a back edge, is replaced by that value, so a + loop-invariant input does not look loop-defined to CSE, GVN and LICM. +4. Local CSE. +5. GVN. +6. Reassociation, LICM and pointer-induction strength reduction. +7. Local CSE and DCE, including ordinary overwritten-store elimination. +8. CFG simplification, then a final local CSE and DCE over the simplified graph. +9. Full graph verification before dumps and machine lowering. | Level | Scheduled transformations | | --- | --- | | O0 | Construction folds, unreachable-block removal, and mandatory verification; optional optimization passes return without transforming the graph. | -| O2 (default) | SCCP, CFG simplification, local CSE, GVN, reassociation, LICM, strength reduction, and DCE. | +| O2 (default) | SCCP, CFG simplification, block-parameter simplification, local CSE, GVN, reassociation, LICM, strength reduction, and DCE. | Pass-local tables are temporary. O0 does not allocate the optional CSE, GVN, loop, or optimization worklists. Mandatory verification still checks dominance, diff --git a/src/vir-frontend.c b/src/vir-frontend.c index 48401c7f..f6ac23b1 100644 --- a/src/vir-frontend.c +++ b/src/vir-frontend.c @@ -1190,6 +1190,7 @@ void vir_frontend_optimize(vir_function_t *graph) fatal("Invalid VIR reachability"); check_native_pass("sccp", vir_sccp(graph, level)); check_native_pass("cfg", vir_simplify_cfg(graph, level)); + check_native_pass("params", vir_simplify_params(graph, level)); check_native_pass("cse", vir_local_cse(graph, level)); check_native_pass("gvn", vir_gvn_with_stats(graph, level, NULL)); check_native_pass("reassociate", vir_reassociate(graph, level)); diff --git a/src/vir.c b/src/vir.c index 7e942161..0a3c2ad6 100644 --- a/src/vir.c +++ b/src/vir.c @@ -4073,32 +4073,46 @@ static bool vir_dominance_init(const vir_function_t *func, return true; } -/* Drop loop-invariant and forwarding parameters without source SSA bindings. */ +/* The one value @param, the @index-th parameter of @block, receives on every + * incoming edge apart from itself, or NULL when the edges disagree. + */ +static vir_value_t *vir_param_trivial_value(const vir_block_t *block, + const vir_value_t *param, + int index) +{ + vir_value_t *candidate = NULL; + + for (vir_edge_t *edge = block->incoming; edge; edge = edge->next_incoming) { + vir_value_t *value = edge->args[index]; + if (value == param) + continue; + if (candidate && candidate != value) + return NULL; + candidate = value; + } + return candidate; +} + +/* Drop loop-invariant and forwarding parameters without source SSA bindings. + * The entry block's parameters are the function's arguments, whose caller + * supplies a value no edge shows, so they always stay. + */ static int vir_simplify_params_with_context(vir_function_t *func, const vir_dominance_t *dominance) { int changed = 0, previous; do { previous = changed; - for (vir_block_t *block = func->blocks; block; block = block->next) { + for (vir_block_t *block = func->blocks->next; block; + block = block->next) { int index = 0; for (vir_value_t *param = block->params, *next; param; param = next) { - vir_value_t *candidate = NULL; - bool trivial = true; + vir_value_t *candidate = + vir_param_trivial_value(block, param, index); next = param->param_next; - for (vir_edge_t *edge = block->incoming; edge; - edge = edge->next_incoming) { - vir_value_t *value = edge->args[index]; - if (value == param) - continue; - if (candidate && candidate != value) - trivial = false; - candidate = value; - } - if (!trivial || !candidate || - !vir_replacement_dominates_with_context(func, dominance, - param, candidate)) { + if (!candidate || !vir_replacement_dominates_with_context( + func, dominance, param, candidate)) { index++; continue; } @@ -4117,6 +4131,32 @@ static int vir_simplify_params_with_context(vir_function_t *func, return changed; } +/* Dominance is built only once some parameter has a single incoming value. */ +int vir_simplify_params(vir_function_t *func, vir_opt_level_t opt_level) +{ + vir_dominance_t dominance; + int changed; + + if (!func || !vir_opt_level_valid(opt_level)) + return -1; + if (opt_level == VIR_OPT_O0 || !func->blocks) + return 0; + for (vir_block_t *block = func->blocks->next; block; block = block->next) { + int index = 0; + for (vir_value_t *param = block->params; param; + param = param->param_next, index++) + if (vir_param_trivial_value(block, param, index)) + goto simplify; + } + return 0; +simplify: + if (!vir_dominance_init(func, &dominance)) + return -1; + changed = vir_simplify_params_with_context(func, &dominance); + vir_dominance_release(&dominance); + return changed; +} + /* An affine offset is safe to recur only while every narrower intermediate * stays nonnegative and fits its signed type. Pointer-width arithmetic itself * is modular, so ILP32 does not need the LP64 narrowing proof. diff --git a/src/vir.h b/src/vir.h index a52a4de5..5c7e4c90 100644 --- a/src/vir.h +++ b/src/vir.h @@ -416,6 +416,14 @@ int vir_simplify_cfg(vir_function_t *func, vir_opt_level_t opt_level); int vir_simplify_cfg_with_pruning(vir_function_t *func, vir_opt_level_t opt_level, bool prune_unreachable); + +/* At O1/O2, replace each block parameter that receives one value on every edge, + * or itself on a back edge, by that value. The entry block's parameters are the + * function's arguments and stay. A loop-invariant input otherwise reaches CSE, + * GVN and LICM as a value defined inside the loop. The CFG is unchanged. + */ +int vir_simplify_params(vir_function_t *func, vir_opt_level_t opt_level); + int vir_block_set_branch(vir_function_t *func, vir_block_t *block, vir_value_t *condition, diff --git a/tests/vir-core.c b/tests/vir-core.c index 7a057db9..32354725 100644 --- a/tests/vir-core.c +++ b/tests/vir-core.c @@ -2273,6 +2273,52 @@ int main(void) assert(vir_verify(&func, &error)); vir_function_release(&func); + /* A header parameter that only carries an input around its loop hides a + * product of that input from GVN. Parameter simplification, skipped at O0, + * replaces it by the input; GVN then reuses the product computed before the + * loop inside it. + */ + vir_function_init(&func, 64); + assert(vir_function_set_pointer_bits(&func, 64)); + entry = vir_block_create(&func); + merge = vir_block_create(&func); + otherwise = vir_block_create(&func); + dead = vir_block_create(&func); + entry_param = vir_block_add_param(&func, entry, VIR_TYPE_I1); + one = vir_block_add_param(&func, entry, VIR_TYPE_I32); + two = vir_block_add_param(&func, entry, VIR_TYPE_I32); + stack_duplicate = vir_block_add_param(&func, entry, VIR_TYPE_PTR); + sum = vir_binary(&func, entry, VIR_OP_MUL, VIR_TYPE_I32, one, two); + param = vir_block_add_param(&func, merge, VIR_TYPE_I32); + product = + vir_binary(&func, otherwise, VIR_OP_MUL, VIR_TYPE_I32, param, two); + { + vir_value_t *entry_args[] = {one}; + vir_value_t *backedge_args[] = {param}; + vir_edge_args_t loop_true = {otherwise, NULL, 0}; + vir_edge_args_t loop_false = {dead, NULL, 0}; + vir_cse_stats_t gvn_stats; + + assert(entry_param && one && two && sum && param && product && + stack_duplicate && + vir_store(&func, otherwise, stack_duplicate, product) && + vir_edge_create(&func, entry, merge, entry_args, 1) && + vir_edge_create(&func, otherwise, merge, backedge_args, 1) && + vir_block_set_branch(&func, merge, entry_param, &loop_true, + &loop_false) && + vir_block_set_return(&func, dead, sum) && + vir_verify(&func, &error)); + assert(vir_gvn_with_stats(&func, VIR_OPT_O2, &gvn_stats) == 0 && + product->uses && merge->param_count == 1); + assert(vir_simplify_params(&func, VIR_OPT_O0) == 0 && + merge->param_count == 1); + assert(vir_simplify_params(&func, VIR_OPT_O2) == 1 && + merge->param_count == 0 && product->op0 == one); + assert(vir_gvn_with_stats(&func, VIR_OPT_O2, &gvn_stats) == 1 && + !product->uses && sum->uses && vir_verify(&func, &error)); + } + vir_function_release(&func); + /* LICM derives a natural loop only at O2 and hoists an effect-free value * with operands defined outside the loop into its single-edge preheader. */ @@ -2397,6 +2443,73 @@ int main(void) } vir_function_release(&func); + /* An input that reaches the header unchanged around the loop sits behind a + * header parameter, so LICM treats a value computed from it as + * loop-defined. Once parameter simplification drops it, the value moves to + * the preheader. + */ + vir_function_init(&func, 64); + entry = vir_block_create(&func); + merge = vir_block_create(&func); + otherwise = vir_block_create(&func); + dead = vir_block_create(&func); + entry_param = vir_block_add_param(&func, entry, VIR_TYPE_I1); + one = vir_block_add_param(&func, entry, VIR_TYPE_I32); + two = vir_block_add_param(&func, entry, VIR_TYPE_I32); + param = vir_block_add_param(&func, merge, VIR_TYPE_I32); + product = + vir_binary(&func, otherwise, VIR_OP_MUL, VIR_TYPE_I32, param, two); + { + vir_value_t *entry_args[] = {one}; + vir_value_t *backedge_args[] = {param}; + vir_edge_args_t loop_true = {otherwise, NULL, 0}; + vir_edge_args_t loop_false = {dead, NULL, 0}; + + assert(entry && merge && otherwise && dead && entry_param && one && + two && param && product && + vir_edge_create(&func, entry, merge, entry_args, 1) && + vir_edge_create(&func, otherwise, merge, backedge_args, 1) && + vir_block_set_branch(&func, merge, entry_param, &loop_true, + &loop_false) && + vir_block_set_return(&func, dead, param) && + vir_verify(&func, &error)); + assert(vir_licm(&func, VIR_OPT_O2) == 0 && product->block == otherwise); + assert(vir_simplify_params(&func, VIR_OPT_O2) == 1 && + merge->param_count == 0 && product->op0 == one); + assert(vir_licm(&func, VIR_OPT_O2) == 1 && product->block == entry && + vir_verify(&func, &error)); + } + vir_function_release(&func); + + /* The entry block's parameters are the function's arguments. A back edge + * into the entry block passing one value must not replace an argument. + */ + vir_function_init(&func, 64); + entry = vir_block_create(&func); + merge = vir_block_create(&func); + dead = vir_block_create(&func); + one = vir_block_add_param(&func, entry, VIR_TYPE_I32); + entry_param = vir_block_add_param(&func, entry, VIR_TYPE_I1); + two = vir_const_i32(&func, merge, 5); + sum = vir_binary(&func, merge, VIR_OP_ADD, VIR_TYPE_I32, one, two); + { + vir_value_t *backedge_args[] = {two, entry_param}; + vir_edge_args_t loop_true = {entry, backedge_args, 2}; + vir_edge_args_t loop_false = {dead, NULL, 0}; + + assert(entry && merge && dead && one && entry_param && two && sum && + vir_edge_create(&func, entry, merge, NULL, 0) && + vir_block_set_branch(&func, merge, entry_param, &loop_true, + &loop_false) && + vir_block_set_return(&func, dead, sum) && + vir_verify(&func, &error)); + assert(vir_simplify_params(&func, VIR_OPT_O2) == 0 && + entry->param_count == 2 && + (sum->op0 == one || sum->op1 == one) && + vir_verify(&func, &error)); + } + vir_function_release(&func); + /* LICM's pass-local dominator bitsets keep sparse large-CFG storage below * the former four-byte-per-block-pair matrix while retaining loop motion. */ From 17421114e409b42fce46e45ae335f35bb218a737 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Thu, 15 Oct 2026 22:49:52 +0800 Subject: [PATCH 2/2] Fold branches whose arms reach one block When CFG simplification bypasses the empty arms of a diamond such as if (c) y = x; else y = x; both edges of the branch end up on the join with the same arguments. The branch was kept, so its condition stayed live and x86-64 emitted "test edi, edi; jne +0" in front of the join. CFG simplification now turns such a branch into a jump, and DCE drops the condition. Edges that pass different arguments still select a value and stay a branch. Folding only deletes an edge, so the scan continues in place instead of restarting from the first block. Close #290 --- src/vir.c | 24 ++++++++++++++++++++++++ src/vir.h | 11 ++++++----- tests/vir-core.c | 41 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/vir.c b/src/vir.c index 0a3c2ad6..5d3fe906 100644 --- a/src/vir.c +++ b/src/vir.c @@ -5076,6 +5076,22 @@ static bool vir_fold_branch_condition(vir_block_t *block) return true; } +/* A branch whose two edges reach the same block with the same arguments goes + * there either way, so it is a jump, and DCE can drop its condition. + */ +static int vir_fold_same_target_branch(vir_function_t *func, vir_block_t *block) +{ + vir_edge_t *taken = block->true_edge, *other = block->false_edge; + + if (block->terminator != VIR_TERM_BRANCH || taken->to != other->to || + taken->arg_count != other->arg_count) + return 0; + for (int i = 0; i < taken->arg_count; i++) + if (taken->args[i] != other->args[i]) + return 0; + return vir_edge_delete(func, other) ? 1 : -1; +} + /* An incoming constant can select a branch without materializing its boolean * parameter. Keep the bypassed computations local and successors argument-free. */ @@ -5157,6 +5173,14 @@ int vir_simplify_cfg_with_pruning(vir_function_t *func, vir_block_t *target; int inlined; + /* Folding only drops an edge, so the scan can go on in place. */ + inlined = vir_fold_same_target_branch(func, block); + if (inlined < 0) + return -1; + if (inlined) { + changes++; + changed = true; + } inlined = vir_thread_constant_edge(func, block); if (!inlined) inlined = vir_inline_return_param(func, block); diff --git a/src/vir.h b/src/vir.h index 5c7e4c90..55bbbb1e 100644 --- a/src/vir.h +++ b/src/vir.h @@ -406,11 +406,12 @@ int vir_remove_unreachable(vir_function_t *func); int vir_function_block_count(const vir_function_t *func); /* At O1/O2, bypass empty jump blocks and local branches selected by constant - * incoming arguments. The regular helper also drops newly unreachable blocks; - * the explicit helper can defer that deletion for a caller that must run - * another pass on the rewritten topology. Branch threading excludes effects and - * successor arguments. A deferred caller must prune before lowering (and before - * any final reachability-sensitive verification). + * incoming arguments, and turn a branch whose arms reach one block with the + * same arguments into a jump. The regular helper also drops newly unreachable + * blocks; the explicit helper can defer that deletion for a caller that must + * run another pass on the rewritten topology. Branch threading excludes effects + * and successor arguments. A deferred caller must prune before lowering (and + * before any final reachability-sensitive verification). */ int vir_simplify_cfg(vir_function_t *func, vir_opt_level_t opt_level); int vir_simplify_cfg_with_pruning(vir_function_t *func, diff --git a/tests/vir-core.c b/tests/vir-core.c index 32354725..31152c5a 100644 --- a/tests/vir-core.c +++ b/tests/vir-core.c @@ -2631,7 +2631,8 @@ int main(void) vir_function_release(&func); /* CFG simplification redirects each predecessor through an empty jump - * block, preserves a duplicate branch target, and removes the forwarder. + * block, which leaves both branch arms on one target, collapses that branch + * into a jump, and removes the forwarders. */ vir_function_init(&func, 64); entry = vir_block_create(&func); @@ -2651,12 +2652,48 @@ int main(void) &cfg_false_desc)); assert(vir_simplify_cfg(&func, VIR_OPT_O0) == 0); assert(vir_simplify_cfg(&func, VIR_OPT_O1) > 0 && - entry->true_edge->to == dead && entry->false_edge->to == dead && + entry->terminator == VIR_TERM_JUMP && entry->outgoing && + entry->outgoing->to == dead && !entry->outgoing->next_outgoing && func.blocks == entry && entry->next == dead && !dead->next); } assert(vir_verify(&func, &error)); vir_function_release(&func); + /* Both arms on one target fold to a jump only when they also pass the same + * arguments; different arguments still select a value and stay a branch. + */ + for (int same = 0; same < 2; same++) { + vir_function_init(&func, 64); + entry = vir_block_create(&func); + merge = vir_block_create(&func); + entry_param = vir_block_add_param(&func, entry, VIR_TYPE_I1); + one = vir_const_i32(&func, entry, 1); + two = vir_const_i32(&func, entry, 2); + param = vir_block_add_param(&func, merge, VIR_TYPE_I32); + vir_edge_args_t arm_true = {merge, &one, 1}; + vir_edge_args_t arm_false = {merge, same ? &one : &two, 1}; + + assert(entry && merge && entry_param && one && two && param && + vir_block_set_branch(&func, entry, entry_param, &arm_true, + &arm_false) && + vir_block_set_return(&func, merge, param)); + + /* The folded jump then feeds a block that only returns its parameter, + * so CFG simplification goes on to return the value from the entry. + */ + if (same) + assert(vir_simplify_cfg(&func, VIR_OPT_O2) > 0 && + entry->terminator == VIR_TERM_RETURN && + entry->return_value == one && !entry_param->uses); + else + assert(vir_simplify_cfg(&func, VIR_OPT_O2) == 0 && + entry->terminator == VIR_TERM_BRANCH && + entry->true_edge->args[0] == one && + entry->false_edge->args[0] == two); + assert(vir_verify(&func, &error)); + vir_function_release(&func); + } + /* Branches on promoted booleans preserve polarity and reverse-use links. */ for (int width = 0; width < 2; width++) { for (int constant = 0; constant < 3; constant++) {