Skip to content

Commit 7768e74

Browse files
committed
[WIP / RFC] Cranelift: Basic support for EGraph roundtripping.
This is a work-in-progress, and meant to sketch the direction I've been thinking in for a mid-end framework. A proper BA RFC will come soon. This PR builds a phase in the optimization pipeline that converts a CLIF CFG into an egraph representing the function body. Each node represents an original instruction or operator. The "skeleton" of side-effecting instructions is retained, but non-side-effecting (pure) operators are allowed to "float": the egraph will naturally deduplicate them during build, and we will determine their proper place when we convert back to a CFG representation. The conversion from the egraph back to the CFG is done via a new algorithm I call "scoped elaboration". The basic idea is to do a preorder traversal of the domtree, and at each level, evaluate the values of the eclasses called upon by the side-effect skeleton, memoizing in an eclass-to-SSA-value map. This map is a scoped hashmap, with scopes at each domtree level. In this way, (i) when a value is computed in a location that dominates another instance of that value, the first replacees the second; but (ii) we never produce "partially dead" computations, i.e. we never hoist to a level in the domtree where a node is not "anticipated" (always eventually computed). This exactly matches what GVN does today. With a small tweak, it can also subsume LICM: we need to be loop-nest-aware in our recursive eclass elaboration, and potentially place nodes higher up the domtree (and higher up in the scoped hashmap). Unlike what I had been thinking in Monday's meeting, this produces CLIF out of the egraph and then allows that to be lowered. It's overall simpler and a better starting point (thanks @abrown for tipping me over the edge in this). The way it produces CLIF now could be made more efficient: it could reuse instructions already in the DFG for nodes that are *not* duplicated (likely most of them) rather than clearing all and repopulating. This PR does *not* do anything to actually rewrite in the egraph. That's the next step! I need to work out exactly how to integrate ISLE with some sort of rewrite machinery. I have some ideas about efficient dispatch with an "operand-tree discriminants shape analysis" on the egraph and indexing rules by their matched shape; more to come.
1 parent b830c3c commit 7768e74

25 files changed

Lines changed: 1149 additions & 54 deletions

Cargo.lock

Lines changed: 28 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cranelift/codegen/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ gimli = { version = "0.26.0", default-features = false, features = ["write"], op
2525
smallvec = { version = "1.6.1" }
2626
regalloc2 = { version = "0.2.0", features = ["checker"] }
2727
souper-ir = { version = "2.1.0", optional = true }
28+
egg = { git = "https://github.com/cfallin/egg", branch = "egg-lite", default-features = false }
2829
# It is a goal of the cranelift-codegen crate to have minimal external dependencies.
2930
# Please don't add any unless they are essential to the task of creating binary
3031
# machine code. Integration tests that need external dependencies can be

cranelift/codegen/meta/src/gen_inst.rs

Lines changed: 159 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -60,36 +60,51 @@ fn gen_formats(formats: &[&InstructionFormat], fmt: &mut Formatter) {
6060
fmt.empty_line();
6161
}
6262

63-
/// Generate the InstructionData enum.
63+
/// Generate the InstructionData and InstructionImms enums.
64+
///
65+
/// `InstructionImms` stores everything about an instruction except for the arguments: in other
66+
/// words, the `Opcode` and any immediates or other parameters. `InstructionData` stores this, plus
67+
/// the SSA `Value` arguments.
6468
///
6569
/// Every variant must contain an `opcode` field. The size of `InstructionData` should be kept at
6670
/// 16 bytes on 64-bit architectures. If more space is needed to represent an instruction, use a
6771
/// `ValueList` to store the additional information out of line.
6872
fn gen_instruction_data(formats: &[&InstructionFormat], fmt: &mut Formatter) {
69-
fmt.line("#[derive(Clone, Debug)]");
70-
fmt.line(r#"#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]"#);
71-
fmt.line("#[allow(missing_docs)]");
72-
fmt.line("pub enum InstructionData {");
73-
fmt.indent(|fmt| {
74-
for format in formats {
75-
fmtln!(fmt, "{} {{", format.name);
76-
fmt.indent(|fmt| {
77-
fmt.line("opcode: Opcode,");
78-
if format.has_value_list {
79-
fmt.line("args: ValueList,");
80-
} else if format.num_value_operands == 1 {
81-
fmt.line("arg: Value,");
82-
} else if format.num_value_operands > 0 {
83-
fmtln!(fmt, "args: [Value; {}],", format.num_value_operands);
84-
}
85-
for field in &format.imm_fields {
86-
fmtln!(fmt, "{}: {},", field.member, field.kind.rust_type);
87-
}
88-
});
89-
fmtln!(fmt, "},");
73+
for (name, include_args) in &[("InstructionData", true), ("InstructionImms", false)] {
74+
fmt.line("#[derive(Clone, Debug)]");
75+
if !include_args {
76+
// `InstructionImms` gets some extra derives: it acts like a sort of extended opcode
77+
// and we want to allow for hashconsing, sorting, and the like.
78+
fmt.line("#[derive(Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]");
9079
}
91-
});
92-
fmt.line("}");
80+
fmt.line(r#"#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]"#);
81+
fmt.line("#[allow(missing_docs)]");
82+
// generate `enum InstructionData` or `enum InstructionImms`.
83+
// (This comment exists so one can grep for `enum InstructionData`!)
84+
fmtln!(fmt, "pub enum {} {{", name);
85+
fmt.indent(|fmt| {
86+
for format in formats {
87+
fmtln!(fmt, "{} {{", format.name);
88+
fmt.indent(|fmt| {
89+
fmt.line("opcode: Opcode,");
90+
if *include_args {
91+
if format.has_value_list {
92+
fmt.line("args: ValueList,");
93+
} else if format.num_value_operands == 1 {
94+
fmt.line("arg: Value,");
95+
} else if format.num_value_operands > 0 {
96+
fmtln!(fmt, "args: [Value; {}],", format.num_value_operands);
97+
}
98+
}
99+
for field in &format.imm_fields {
100+
fmtln!(fmt, "{}: {},", field.member, field.kind.rust_type);
101+
}
102+
});
103+
fmtln!(fmt, "},");
104+
}
105+
});
106+
fmt.line("}");
107+
}
93108
}
94109

95110
fn gen_arguments_method(formats: &[&InstructionFormat], fmt: &mut Formatter, is_mut: bool) {
@@ -150,6 +165,122 @@ fn gen_arguments_method(formats: &[&InstructionFormat], fmt: &mut Formatter, is_
150165
fmtln!(fmt, "}");
151166
}
152167

168+
/// Generate the conversion from `InstructionData` to `InstructionImms`, stripping out the
169+
/// `Value`s.
170+
fn gen_instruction_data_to_instruction_imms(formats: &[&InstructionFormat], fmt: &mut Formatter) {
171+
fmt.line("impl std::convert::From<&InstructionData> for InstructionImms {");
172+
fmt.indent(|fmt| {
173+
fmt.doc_comment("Convert an `InstructionData` into an `InstructionImms`.");
174+
fmt.line("fn from(data: &InstructionData) -> InstructionImms {");
175+
fmt.indent(|fmt| {
176+
fmt.line("match data {");
177+
fmt.indent(|fmt| {
178+
for format in formats {
179+
fmtln!(fmt, "InstructionData::{} {{", format.name);
180+
fmt.indent(|fmt| {
181+
fmt.line("opcode,");
182+
for field in &format.imm_fields {
183+
fmtln!(fmt, "{},", field.member);
184+
}
185+
fmt.line("..");
186+
});
187+
fmtln!(fmt, "}} => InstructionImms::{} {{", format.name);
188+
fmt.indent(|fmt| {
189+
fmt.line("opcode: *opcode,");
190+
for field in &format.imm_fields {
191+
fmtln!(fmt, "{}: {}.clone(),", field.member, field.member);
192+
}
193+
});
194+
fmt.line("},");
195+
}
196+
});
197+
fmt.line("}");
198+
});
199+
fmt.line("}");
200+
});
201+
fmt.line("}");
202+
fmt.empty_line();
203+
}
204+
205+
/// Generate the conversion from `InstructionImms` to `InstructionData`, adding the
206+
/// `Value`s.
207+
fn gen_instruction_imms_to_instruction_data(formats: &[&InstructionFormat], fmt: &mut Formatter) {
208+
fmt.line("impl InstructionImms {");
209+
fmt.indent(|fmt| {
210+
fmt.doc_comment("Convert an `InstructionImms` into an `InstructionData` by adding args.");
211+
fmt.line(
212+
"pub fn with_args(&self, values: &[Value], value_list: &mut ValueListPool) -> InstructionData {",
213+
);
214+
fmt.indent(|fmt| {
215+
fmt.line("match self {");
216+
fmt.indent(|fmt| {
217+
for format in formats {
218+
fmtln!(fmt, "InstructionImms::{} {{", format.name);
219+
fmt.indent(|fmt| {
220+
fmt.line("opcode,");
221+
for field in &format.imm_fields {
222+
fmtln!(fmt, "{},", field.member);
223+
}
224+
});
225+
fmt.line("} => {");
226+
if format.has_value_list {
227+
fmtln!(fmt, "let args = ValueList::from_slice(values, value_list);");
228+
}
229+
fmt.indent(|fmt| {
230+
fmtln!(fmt, "InstructionData::{} {{", format.name);
231+
fmt.indent(|fmt| {
232+
fmt.line("opcode: *opcode,");
233+
for field in &format.imm_fields {
234+
fmtln!(fmt, "{}: {}.clone(),", field.member, field.member);
235+
}
236+
if format.has_value_list {
237+
fmtln!(fmt, "args,");
238+
} else if format.num_value_operands == 1 {
239+
fmtln!(fmt, "arg: values[0],");
240+
} else if format.num_value_operands > 0 {
241+
let mut args = vec![];
242+
for i in 0..format.num_value_operands {
243+
args.push(format!("values[{}]", i));
244+
}
245+
fmtln!(fmt, "args: [{}],", args.join(", "));
246+
}
247+
});
248+
fmt.line("}");
249+
});
250+
fmt.line("},");
251+
}
252+
});
253+
fmt.line("}");
254+
});
255+
fmt.line("}");
256+
});
257+
fmt.line("}");
258+
fmt.empty_line();
259+
}
260+
261+
/// Generate the `opcode` method on InstructionImms.
262+
fn gen_instruction_imms_impl(formats: &[&InstructionFormat], fmt: &mut Formatter) {
263+
fmt.line("impl InstructionImms {");
264+
fmt.indent(|fmt| {
265+
fmt.doc_comment("Get the opcode of this instruction.");
266+
fmt.line("pub fn opcode(&self) -> Opcode {");
267+
fmt.indent(|fmt| {
268+
let mut m = Match::new("*self");
269+
for format in formats {
270+
m.arm(
271+
format!("Self::{}", format.name),
272+
vec!["opcode", ".."],
273+
"opcode".to_string(),
274+
);
275+
}
276+
fmt.add_match(m);
277+
});
278+
fmt.line("}");
279+
});
280+
fmt.line("}");
281+
fmt.empty_line();
282+
}
283+
153284
/// Generate the boring parts of the InstructionData implementation.
154285
///
155286
/// These methods in `impl InstructionData` can be generated automatically from the instruction
@@ -406,7 +537,7 @@ fn gen_opcodes(all_inst: &AllInstructions, fmt: &mut Formatter) {
406537
"#,
407538
);
408539
fmt.line("#[repr(u16)]");
409-
fmt.line("#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]");
540+
fmt.line("#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]");
410541
fmt.line(
411542
r#"#[cfg_attr(
412543
feature = "enable-serde",
@@ -1412,6 +1543,9 @@ pub(crate) fn generate(
14121543
gen_instruction_data(&formats, &mut fmt);
14131544
fmt.empty_line();
14141545
gen_instruction_data_impl(&formats, &mut fmt);
1546+
gen_instruction_data_to_instruction_imms(&formats, &mut fmt);
1547+
gen_instruction_imms_impl(&formats, &mut fmt);
1548+
gen_instruction_imms_to_instruction_data(&formats, &mut fmt);
14151549
fmt.empty_line();
14161550
gen_opcodes(all_inst, &mut fmt);
14171551
fmt.empty_line();

cranelift/codegen/meta/src/shared/settings.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ pub(crate) fn define() -> SettingGroup {
3131
vec!["none", "speed", "speed_and_size"],
3232
);
3333

34+
settings.add_bool(
35+
"use_egraphs",
36+
"Enable egraph-based optimization.",
37+
r#"
38+
This enables an optimization phase that converts CLIF to an egraph (equivalence graph)
39+
representation, performs various rewrites, and then converts it back. This can result in
40+
better optimization, but at the cost of a longer compile time.
41+
"#,
42+
false,
43+
);
44+
3445
settings.add_bool(
3546
"enable_verifier",
3647
"Run the Cranelift IR verifier at strategic times during compilation.",

0 commit comments

Comments
 (0)