Skip to content

Commit 487d1fc

Browse files
authored
Optimize ISLE compilation (#12303)
* optimize isle codegen for rustc * opt-in for optimizing isle codegen * have a controller threshold
1 parent 9715ddf commit 487d1fc

3 files changed

Lines changed: 132 additions & 6 deletions

File tree

cranelift/codegen/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ disas = ["anyhow", "capstone"]
8181
# Enables detailed logging which can be somewhat expensive.
8282
trace-log = ["regalloc2/trace-log"]
8383

84+
# By default, an ISLE term is compiled into a single Rust function, but it can be
85+
# significantly inefficient for large terms (e.g. `simplify` with hundreds of rules).
86+
# This is because the generated Rust code for such terms is large, and `rustc` takes quadratically longer to compile huge functions.
87+
# This feature splits large match arms in such ISLE terms into closures, for compiling ISLE terms more efficiently.
88+
# However, this can degrade Cranelift compilation times, introducing ABI boundaries between the closures.
89+
# Therefore, we recommend enabling this feature only for debugging/development purposes.
90+
isle-split-match = []
91+
8492
# This enables unwind info generation functionality.
8593
unwind = ["gimli"]
8694

cranelift/codegen/build.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,14 @@ fn run_compilation(compilation: &IsleCompilation) -> Result<(), Errors> {
216216
// the generated code to help debug rule matching.
217217
options.emit_logging = std::env::var("CARGO_FEATURE_TRACE_LOG").is_ok();
218218

219+
// Enable optional match-arm splitting in iterator terms for faster compile times.
220+
options.split_match_arms = std::env::var("CARGO_FEATURE_ISLE_SPLIT_MATCH").is_ok();
221+
if let Ok(value) = std::env::var("ISLE_SPLIT_MATCH_THRESHOLD") {
222+
options.match_arm_split_threshold = Some(value.parse().unwrap_or_else(|err| {
223+
panic!("invalid ISLE_SPLIT_MATCH_THRESHOLD value '{value}': {err}");
224+
}));
225+
}
226+
219227
if let Ok(out_dir) = std::env::var("OUT_DIR") {
220228
options.prefixes.push(isle::codegen::Prefix {
221229
prefix: out_dir,

cranelift/isle/isle/src/codegen.rs

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use std::fmt::Write;
1111
use std::slice::Iter;
1212
use std::sync::Arc;
1313

14+
const DEFAULT_MATCH_ARM_BODY_CLOSURE_THRESHOLD: usize = 256;
15+
1416
/// Options for code generation.
1517
#[derive(Clone, Debug, Default)]
1618
pub struct CodegenOptions {
@@ -28,6 +30,17 @@ pub struct CodegenOptions {
2830
/// In Cranelift this is typically controlled by a cargo feature on the
2931
/// crate that includes the generated code (e.g. `cranelift-codegen`).
3032
pub emit_logging: bool,
33+
34+
/// Split large match arms into local closures when generating iterator terms.
35+
///
36+
/// In Cranelift this is typically controlled by a cargo feature on the
37+
/// crate that includes the generated code (e.g. `cranelift-codegen`).
38+
pub split_match_arms: bool,
39+
40+
/// Threshold for splitting match arms into local closures.
41+
///
42+
/// If `None`, a default threshold is used.
43+
pub match_arm_split_threshold: Option<usize>,
3144
}
3245

3346
/// A path prefix which should be replaced when printing file names.
@@ -72,10 +85,29 @@ struct BodyContext<'a, W> {
7285
is_bound: StableSet<BindingId>,
7386
term_name: &'a str,
7487
emit_logging: bool,
88+
split_match_arms: bool,
89+
match_arm_split_threshold: Option<usize>,
90+
91+
// Extra fields for iterator-returning terms.
92+
// These fields are used to generate optimized Rust code for iterator-returning terms.
93+
/// The number of match splits that have been generated.
94+
/// This is used to generate unique names for the match splits.
95+
match_split: usize,
96+
97+
/// The action to take when the iterator overflows.
98+
iter_overflow_action: &'static str,
7599
}
76100

77101
impl<'a, W: Write> BodyContext<'a, W> {
78-
fn new(out: &'a mut W, ruleset: &'a RuleSet, term_name: &'a str, emit_logging: bool) -> Self {
102+
fn new(
103+
out: &'a mut W,
104+
ruleset: &'a RuleSet,
105+
term_name: &'a str,
106+
emit_logging: bool,
107+
split_match_arms: bool,
108+
match_arm_split_threshold: Option<usize>,
109+
iter_overflow_action: &'static str,
110+
) -> Self {
79111
Self {
80112
out,
81113
ruleset,
@@ -84,6 +116,10 @@ impl<'a, W: Write> BodyContext<'a, W> {
84116
is_bound: Default::default(),
85117
term_name,
86118
emit_logging,
119+
split_match_arms,
120+
match_arm_split_threshold,
121+
match_split: Default::default(),
122+
iter_overflow_action,
87123
}
88124
}
89125

@@ -426,7 +462,19 @@ impl<L: Length, C> Length for ContextIterWrapper<L, C> {{
426462

427463
let termdata = &self.termenv.terms[termid.index()];
428464
let term_name = &self.typeenv.syms[termdata.name.index()];
429-
let mut ctx = BodyContext::new(code, ruleset, term_name, options.emit_logging);
465+
466+
// Split a match if the term returns an iterator.
467+
let mut ctx = BodyContext::new(
468+
code,
469+
ruleset,
470+
term_name,
471+
options.emit_logging,
472+
options.split_match_arms,
473+
options.match_arm_split_threshold,
474+
"return;", // At top level, we just return.
475+
);
476+
477+
// Generate the function signature.
430478
writeln!(ctx.out)?;
431479
writeln!(
432480
ctx.out,
@@ -470,6 +518,7 @@ impl<L: Length, C> Length for ContextIterWrapper<L, C> {{
470518
ReturnKind::Option => write!(ctx.out, "Option<{ret}>")?,
471519
ReturnKind::Plain => write!(ctx.out, "{ret}")?,
472520
};
521+
// Generating the function signature is done.
473522

474523
let last_expr = if let Some(EvalStep {
475524
check: ControlFlow::Return { .. },
@@ -530,6 +579,21 @@ impl<L: Length, C> Length for ContextIterWrapper<L, C> {{
530579
Nested::Cases(block.steps.iter())
531580
}
532581

582+
fn block_weight(block: &Block) -> usize {
583+
fn cf_weight(cf: &ControlFlow) -> usize {
584+
match cf {
585+
ControlFlow::Match { arms, .. } => {
586+
arms.iter().map(|a| Codegen::block_weight(&a.body)).sum()
587+
}
588+
ControlFlow::Equal { body, .. } => Codegen::block_weight(body),
589+
ControlFlow::Loop { body, .. } => Codegen::block_weight(body),
590+
ControlFlow::Return { .. } => 0,
591+
}
592+
}
593+
594+
block.steps.iter().map(|s| 1 + cf_weight(&s.check)).sum()
595+
}
596+
533597
fn emit_block<W: Write>(
534598
&self,
535599
ctx: &mut BodyContext<W>,
@@ -538,8 +602,19 @@ impl<L: Length, C> Length for ContextIterWrapper<L, C> {{
538602
last_expr: &str,
539603
scope: StableSet<BindingId>,
540604
) -> std::fmt::Result {
541-
let mut stack = Vec::new();
542605
ctx.begin_block()?;
606+
self.emit_block_contents(ctx, block, ret_kind, last_expr, scope)
607+
}
608+
609+
fn emit_block_contents<W: Write>(
610+
&self,
611+
ctx: &mut BodyContext<W>,
612+
block: &Block,
613+
ret_kind: ReturnKind,
614+
last_expr: &str,
615+
scope: StableSet<BindingId>,
616+
) -> std::fmt::Result {
617+
let mut stack = Vec::new();
543618
stack.push((Self::validate_block(ret_kind, block), last_expr, scope));
544619

545620
while let Some((mut nested, last_line, scope)) = stack.pop() {
@@ -706,8 +781,8 @@ impl<L: Length, C> Length for ContextIterWrapper<L, C> {{
706781
writeln!(ctx.out, "));")?;
707782
writeln!(
708783
ctx.out,
709-
"{}if returns.len() >= MAX_ISLE_RETURNS {{ return; }}",
710-
ctx.indent
784+
"{}if returns.len() >= MAX_ISLE_RETURNS {{ {} }}",
785+
ctx.indent, ctx.iter_overflow_action
711786
)?;
712787
}
713788
}
@@ -729,7 +804,42 @@ impl<L: Length, C> Length for ContextIterWrapper<L, C> {{
729804
self.emit_constraint(ctx, source, arm)?;
730805
write!(ctx.out, " =>")?;
731806
ctx.begin_block()?;
732-
stack.push((Self::validate_block(ret_kind, &arm.body), "", scope));
807+
808+
// Compile-time optimization: huge function bodies (often from very large match arms
809+
// of constructor bodies)cause rustc to spend a lot of time in analysis passes.
810+
// Wrap such bodies in a local closure to move the bulk of the work into a separate body
811+
// without needing to know the types of captured locals.
812+
let match_arm_body_closure_threshold = ctx
813+
.match_arm_split_threshold
814+
.unwrap_or(DEFAULT_MATCH_ARM_BODY_CLOSURE_THRESHOLD);
815+
if ctx.split_match_arms
816+
&& ret_kind == ReturnKind::Iterator
817+
&& Codegen::block_weight(&arm.body) > match_arm_body_closure_threshold
818+
{
819+
let closure_id = ctx.match_split;
820+
ctx.match_split += 1;
821+
822+
write!(ctx.out, "{}if (|| -> bool", &ctx.indent)?;
823+
ctx.begin_block()?;
824+
825+
let old_overflow_action = ctx.iter_overflow_action;
826+
ctx.iter_overflow_action = "return true;";
827+
let closure_scope = ctx.enter_scope();
828+
self.emit_block_contents(ctx, &arm.body, ret_kind, "false", closure_scope)?;
829+
ctx.iter_overflow_action = old_overflow_action;
830+
831+
// Close `if (|| -> bool { ... })()` and stop the outer function on
832+
// iterator-overflow.
833+
writeln!(
834+
ctx.out,
835+
"{})() {{ {} }} // __isle_arm_{}",
836+
&ctx.indent, ctx.iter_overflow_action, closure_id
837+
)?;
838+
839+
ctx.end_block("", scope)?;
840+
} else {
841+
stack.push((Self::validate_block(ret_kind, &arm.body), "", scope));
842+
}
733843
}
734844
}
735845
}

0 commit comments

Comments
 (0)