Start a very simple GVN pass (#79)

* Skeleton simple_gvn pass.
* Basic testing infrastructure for simple-gvn.
* Add can_load and can_store flags to instructions.
* Move the replace_values function into the DataFlowGraph.
* Make InstructionData derive from Hash, PartialEq, and Eq.
* Make EntityList's hash and eq functions panic.
* Change Ieee32 and Ieee64 to store u32 and u64, respectively.
This commit is contained in:
Dan Gohman
2017-05-18 18:18:57 -07:00
committed by Jakob Stoklund Olesen
parent 0c7b2c7b68
commit dc809628f4
16 changed files with 256 additions and 57 deletions

View File

@@ -344,3 +344,11 @@ sequences. Instead the test will fail.
Value locations must be present if they are required to compute the binary
bits. Missing value locations will cause the test to crash.
`test simple-gvn`
-----------------
Test the simple GVN pass.
The simple GVN pass is run on each function, and then results are run
through filecheck.

View File

@@ -0,0 +1,11 @@
test simple-gvn
function simple_redundancy(i32, i32) -> i32 {
ebb0(v0: i32, v1: i32):
v2 = iadd v0, v1
v3 = iadd v0, v1
; check: v3 -> v2
v4 = imul v2, v3
; check: v4 = imul $v2, $v3
return v4
}

View File

@@ -201,7 +201,7 @@ load = Instruction(
This is a polymorphic instruction that can load any value type which
has a memory representation.
""",
ins=(Flags, p, Offset), outs=a)
ins=(Flags, p, Offset), outs=a, can_load=True)
store = Instruction(
'store', r"""
@@ -210,7 +210,7 @@ store = Instruction(
This is a polymorphic instruction that can store any value type with a
memory representation.
""",
ins=(Flags, x, p, Offset))
ins=(Flags, x, p, Offset), can_store=True)
iExt8 = TypeVar(
'iExt8', 'An integer type with more than 8 bits',
@@ -224,7 +224,7 @@ uload8 = Instruction(
This is equivalent to ``load.i8`` followed by ``uextend``.
""",
ins=(Flags, p, Offset), outs=a)
ins=(Flags, p, Offset), outs=a, can_load=True)
sload8 = Instruction(
'sload8', r"""
@@ -232,7 +232,7 @@ sload8 = Instruction(
This is equivalent to ``load.i8`` followed by ``uextend``.
""",
ins=(Flags, p, Offset), outs=a)
ins=(Flags, p, Offset), outs=a, can_load=True)
istore8 = Instruction(
'istore8', r"""
@@ -240,7 +240,7 @@ istore8 = Instruction(
This is equivalent to ``ireduce.i8`` followed by ``store.i8``.
""",
ins=(Flags, x, p, Offset))
ins=(Flags, x, p, Offset), can_store=True)
iExt16 = TypeVar(
'iExt16', 'An integer type with more than 16 bits',
@@ -254,7 +254,7 @@ uload16 = Instruction(
This is equivalent to ``load.i16`` followed by ``uextend``.
""",
ins=(Flags, p, Offset), outs=a)
ins=(Flags, p, Offset), outs=a, can_load=True)
sload16 = Instruction(
'sload16', r"""
@@ -262,7 +262,7 @@ sload16 = Instruction(
This is equivalent to ``load.i16`` followed by ``uextend``.
""",
ins=(Flags, p, Offset), outs=a)
ins=(Flags, p, Offset), outs=a, can_load=True)
istore16 = Instruction(
'istore16', r"""
@@ -270,7 +270,7 @@ istore16 = Instruction(
This is equivalent to ``ireduce.i16`` followed by ``store.i8``.
""",
ins=(Flags, x, p, Offset))
ins=(Flags, x, p, Offset), can_store=True)
iExt32 = TypeVar(
'iExt32', 'An integer type with more than 32 bits',
@@ -284,7 +284,7 @@ uload32 = Instruction(
This is equivalent to ``load.i32`` followed by ``uextend``.
""",
ins=(Flags, p, Offset), outs=a)
ins=(Flags, p, Offset), outs=a, can_load=True)
sload32 = Instruction(
'sload32', r"""
@@ -292,7 +292,7 @@ sload32 = Instruction(
This is equivalent to ``load.i32`` followed by ``uextend``.
""",
ins=(Flags, p, Offset), outs=a)
ins=(Flags, p, Offset), outs=a, can_load=True)
istore32 = Instruction(
'istore32', r"""
@@ -300,7 +300,7 @@ istore32 = Instruction(
This is equivalent to ``ireduce.i32`` followed by ``store.i8``.
""",
ins=(Flags, x, p, Offset))
ins=(Flags, x, p, Offset), can_store=True)
x = Operand('x', Mem, doc='Value to be stored')
a = Operand('a', Mem, doc='Value loaded')
@@ -316,7 +316,7 @@ stack_load = Instruction(
access cannot go out of bounds, i.e.
:math:`sizeof(a) + Offset <= sizeof(SS)`.
""",
ins=(SS, Offset), outs=a)
ins=(SS, Offset), outs=a, can_load=True)
stack_store = Instruction(
'stack_store', r"""
@@ -329,7 +329,7 @@ stack_store = Instruction(
access cannot go out of bounds, i.e.
:math:`sizeof(a) + Offset <= sizeof(SS)`.
""",
ins=(x, SS, Offset))
ins=(x, SS, Offset), can_store=True)
stack_addr = Instruction(
'stack_addr', r"""
@@ -357,7 +357,7 @@ heap_load = Instruction(
Trap if the heap access would be out of bounds.
""",
ins=(p, Offset), outs=a)
ins=(p, Offset), outs=a, can_load=True)
heap_store = Instruction(
'heap_store', r"""
@@ -365,7 +365,7 @@ heap_store = Instruction(
Trap if the heap access would be out of bounds.
""",
ins=(x, p, Offset))
ins=(x, p, Offset), can_store=True)
heap_addr = Instruction(
'heap_addr', r"""

View File

@@ -85,6 +85,8 @@ class Instruction(object):
:param is_call: This is a call instruction.
:param is_return: This is a return instruction.
:param can_trap: This instruction can trap.
:param can_load: This instruction can load from memory.
:param can_store: This instruction can store to memory.
"""
# Boolean instruction attributes that can be passed as keyword arguments to
@@ -95,6 +97,8 @@ class Instruction(object):
'is_branch': 'True for all branch or jump instructions.',
'is_call': 'Is this a call instruction?',
'is_return': 'Is this a return instruction?',
'can_load': 'Can this instruction read from memory?',
'can_store': 'Can this instruction write to memory?',
'can_trap': 'Can this instruction cause a trap?',
}

View File

@@ -228,7 +228,7 @@ def gen_opcodes(groups, fmt):
fmt.doc_comment('An instruction opcode.')
fmt.doc_comment('')
fmt.doc_comment('All instructions from all supported ISAs are present.')
fmt.line('#[derive(Copy, Clone, PartialEq, Eq, Debug)]')
fmt.line('#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]')
instrs = []
# We explicitly set the discriminant of the first variant to 1, which

View File

@@ -17,6 +17,7 @@ use legalize_function;
use regalloc;
use result::CtonResult;
use verifier;
use simple_gvn::do_simple_gvn;
/// Persistent data structures and compilation pipeline.
pub struct Context {
@@ -51,16 +52,16 @@ impl Context {
///
/// Also check that the dominator tree and control flow graph are consistent with the function.
///
/// The `TargetIsa` argument is currently unused, but the verifier will soon be able to also
/// The `isa` argument is currently unused, but the verifier will soon be able to also
/// check ISA-dependent constraints.
pub fn verify<'a, ISA: Into<Option<&'a TargetIsa>>>(&self, isa: ISA) -> verifier::Result {
verifier::verify_context(&self.func, &self.cfg, &self.domtree, isa.into())
pub fn verify<'a>(&self, isa: Option<&TargetIsa>) -> verifier::Result {
verifier::verify_context(&self.func, &self.cfg, &self.domtree, isa)
}
/// Run the verifier only if the `enable_verifier` setting is true.
pub fn verify_if(&self, isa: &TargetIsa) -> CtonResult {
if isa.flags().enable_verifier() {
self.verify(isa).map_err(Into::into)
self.verify(Some(isa)).map_err(Into::into)
} else {
Ok(())
}
@@ -78,6 +79,14 @@ impl Context {
self.domtree.compute(&self.func, &self.cfg);
}
/// Perform simple GVN on the function.
pub fn simple_gvn(&mut self) -> CtonResult {
do_simple_gvn(&mut self.func, &mut self.cfg);
// TODO: Factor things such that we can get a Flags and test
// enable_verifier().
self.verify(None).map_err(Into::into)
}
/// Run the register allocator.
pub fn regalloc(&mut self, isa: &TargetIsa) -> CtonResult {
self.regalloc

View File

@@ -46,6 +46,7 @@
//! The index stored in an `EntityList` points to part 2, the list elements. The value 0 is
//! reserved for the empty list which isn't allocated in the vector.
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::mem;
@@ -59,6 +60,10 @@ use entity_map::EntityRef;
/// Entity lists can be cloned, but that operation should only be used as part of cloning the whole
/// function they belong to. *Cloning an entity list does not allocate new memory for the clone*.
/// It creates an alias of the same memory.
///
/// Entity lists can also be hashed and compared for equality, but those operations just panic if,
/// they're ever actually called, because it's not possible to compare the contents of the list
/// without the pool reference.
#[derive(Clone, Debug)]
pub struct EntityList<T: EntityRef> {
index: u32,
@@ -75,6 +80,19 @@ impl<T: EntityRef> Default for EntityList<T> {
}
}
impl<T: EntityRef> Hash for EntityList<T> {
fn hash<H: Hasher>(&self, _: &mut H) {
panic!("hash called on EntityList");
}
}
impl<T: EntityRef> PartialEq for EntityList<T> {
fn eq(&self, _: &EntityList<T>) -> bool {
panic!("eq called on EntityList");
}
}
impl<T: EntityRef> Eq for EntityList<T> {}
/// A memory pool for storing lists of `T`.
#[derive(Clone)]
pub struct ListPool<T: EntityRef> {

View File

@@ -27,7 +27,7 @@ pub trait CondCode: Copy {
/// This condition code is used by the `icmp` instruction to compare integer values. There are
/// separate codes for comparing the integers as signed or unsigned numbers where it makes a
/// difference.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum IntCC {
/// `==`.
Equal,
@@ -139,7 +139,7 @@ impl FromStr for IntCC {
/// The condition codes described here are used to produce a single boolean value from the
/// comparison. The 14 condition codes here cover every possible combination of the relation above
/// except the impossible `!UN & !EQ & !LT & !GT` and the always true `UN | EQ | LT | GT`.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum FloatCC {
/// EQ | LT | GT
Ordered,

View File

@@ -243,6 +243,46 @@ impl DataFlowGraph {
self.values[dest] = ValueData::Alias { ty, original };
}
/// Replace the results of one instruction with aliases to the results of another.
///
/// Change all the results of `dest_inst` to behave as aliases of
/// corresponding results of `src_inst`, as if calling change_to_alias for
/// each.
///
/// After calling this instruction, `dest_inst` will have had its results
/// cleared, so it likely needs to be removed from the graph.
///
pub fn replace_with_aliases(&mut self, dest_inst: Inst, src_inst: Inst) {
debug_assert_ne!(dest_inst,
src_inst,
"Replacing {} with itself would create a loop",
dest_inst);
debug_assert_eq!(self.results[dest_inst].len(&self.value_lists),
self.results[src_inst].len(&self.value_lists),
"Replacing {} with {} would produce a different number of results.",
dest_inst,
src_inst);
for (&dest, &src) in self.results[dest_inst]
.as_slice(&self.value_lists)
.iter()
.zip(self.results[src_inst].as_slice(&self.value_lists)) {
let original = src;
let ty = self.value_type(original);
assert_eq!(self.value_type(dest),
ty,
"Aliasing {} to {} would change its type {} to {}",
dest,
src,
self.value_type(dest),
ty);
self.values[dest] = ValueData::Alias { ty, original };
}
self.clear_results(dest_inst);
}
/// Create a new value alias.
///
/// Note that this function should only be called by the parser.

View File

@@ -14,7 +14,7 @@ use std::str::FromStr;
///
/// An `Imm64` operand can also be used to represent immediate values of smaller integer types by
/// sign-extending to `i64`.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
pub struct Imm64(i64);
impl Imm64 {
@@ -153,7 +153,7 @@ pub type Uimm8 = u8;
///
/// This is used to encode an immediate offset for load/store instructions. All supported ISAs have
/// a maximum load/store offset that fits in an `i32`.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
pub struct Offset32(i32);
impl Offset32 {
@@ -220,7 +220,7 @@ impl FromStr for Offset32 {
/// 32-bit unsigned immediate offset.
///
/// This is used to encode an immediate offset for WebAssembly heap_load/heap_store instructions.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
pub struct Uoffset32(u32);
impl Uoffset32 {
@@ -282,17 +282,19 @@ impl FromStr for Uoffset32 {
}
}
/// An IEEE binary32 immediate floating point value.
/// An IEEE binary32 immediate floating point value, represented as a u32
/// containing the bitpattern.
///
/// All bit patterns are allowed.
#[derive(Copy, Clone, Debug)]
pub struct Ieee32(f32);
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct Ieee32(u32);
/// An IEEE binary64 immediate floating point value.
/// An IEEE binary64 immediate floating point value, represented as a u64
/// containing the bitpattern.
///
/// All bit patterns are allowed.
#[derive(Copy, Clone, Debug)]
pub struct Ieee64(f64);
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct Ieee64(u64);
// Format a floating point number in a way that is reasonably human-readable, and that can be
// converted back to binary without any rounding issues. The hexadecimal formatting of normal and
@@ -531,18 +533,13 @@ fn parse_float(s: &str, w: u8, t: u8) -> Result<u64, &'static str> {
impl Ieee32 {
/// Create a new `Ieee32` representing the number `x`.
pub fn new(x: f32) -> Ieee32 {
Ieee32(x)
}
/// Construct `Ieee32` immediate from raw bits.
pub fn from_bits(x: u32) -> Ieee32 {
Ieee32(unsafe { mem::transmute(x) })
}
}
impl Display for Ieee32 {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let bits: u32 = unsafe { mem::transmute(self.0) };
let bits: u32 = self.0;
format_float(bits as u64, 8, 23, f)
}
}
@@ -552,7 +549,7 @@ impl FromStr for Ieee32 {
fn from_str(s: &str) -> Result<Ieee32, &'static str> {
match parse_float(s, 8, 23) {
Ok(b) => Ok(Ieee32::from_bits(b as u32)),
Ok(b) => Ok(Ieee32(b as u32)),
Err(s) => Err(s),
}
}
@@ -561,18 +558,13 @@ impl FromStr for Ieee32 {
impl Ieee64 {
/// Create a new `Ieee64` representing the number `x`.
pub fn new(x: f64) -> Ieee64 {
Ieee64(x)
}
/// Construct `Ieee64` immediate from raw bits.
pub fn from_bits(x: u64) -> Ieee64 {
Ieee64(unsafe { mem::transmute(x) })
}
}
impl Display for Ieee64 {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let bits: u64 = unsafe { mem::transmute(self.0) };
let bits: u64 = self.0;
format_float(bits, 11, 52, f)
}
}
@@ -582,7 +574,7 @@ impl FromStr for Ieee64 {
fn from_str(s: &str) -> Result<Ieee64, &'static str> {
match parse_float(s, 11, 52) {
Ok(b) => Ok(Ieee64::from_bits(b)),
Ok(b) => Ok(Ieee64(b)),
Err(s) => Err(s),
}
}
@@ -743,11 +735,11 @@ mod tests {
assert_eq!(Ieee32::new(f32::NAN).to_string(), "+NaN");
assert_eq!(Ieee32::new(-f32::NAN).to_string(), "-NaN");
// Construct some qNaNs with payloads.
assert_eq!(Ieee32::from_bits(0x7fc00001).to_string(), "+NaN:0x1");
assert_eq!(Ieee32::from_bits(0x7ff00001).to_string(), "+NaN:0x300001");
assert_eq!(Ieee32(0x7fc00001).to_string(), "+NaN:0x1");
assert_eq!(Ieee32(0x7ff00001).to_string(), "+NaN:0x300001");
// Signaling NaNs.
assert_eq!(Ieee32::from_bits(0x7f800001).to_string(), "+sNaN:0x1");
assert_eq!(Ieee32::from_bits(0x7fa00001).to_string(), "+sNaN:0x200001");
assert_eq!(Ieee32(0x7f800001).to_string(), "+sNaN:0x1");
assert_eq!(Ieee32(0x7fa00001).to_string(), "+sNaN:0x200001");
}
#[test]
@@ -845,14 +837,12 @@ mod tests {
assert_eq!(Ieee64::new(f64::NAN).to_string(), "+NaN");
assert_eq!(Ieee64::new(-f64::NAN).to_string(), "-NaN");
// Construct some qNaNs with payloads.
assert_eq!(Ieee64::from_bits(0x7ff8000000000001).to_string(),
"+NaN:0x1");
assert_eq!(Ieee64::from_bits(0x7ffc000000000001).to_string(),
assert_eq!(Ieee64(0x7ff8000000000001).to_string(), "+NaN:0x1");
assert_eq!(Ieee64(0x7ffc000000000001).to_string(),
"+NaN:0x4000000000001");
// Signaling NaNs.
assert_eq!(Ieee64::from_bits(0x7ff0000000000001).to_string(),
"+sNaN:0x1");
assert_eq!(Ieee64::from_bits(0x7ff4000000000001).to_string(),
assert_eq!(Ieee64(0x7ff0000000000001).to_string(), "+sNaN:0x1");
assert_eq!(Ieee64(0x7ff4000000000001).to_string(),
"+sNaN:0x4000000000001");
}

View File

@@ -98,7 +98,7 @@ impl FromStr for Opcode {
/// value should have its `ty` field set to `VOID`. The size of `InstructionData` should be kept at
/// 16 bytes on 64-bit architectures. If more space is needed to represent an instruction, use a
/// `Box<AuxData>` to store the additional information out of line.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum InstructionData {
Nullary { opcode: Opcode },

View File

@@ -14,7 +14,7 @@ const NAMES: [&str; 2] = ["notrap", "aligned"];
/// Each of these flags introduce a limited form of undefined behavior. The flags each enable
/// certain optimizations that need to make additional assumptions. Generally, the semantics of a
/// program does not change when a flag is removed, but adding a flag will.
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct MemFlags {
bits: u8,
}

View File

@@ -35,5 +35,6 @@ mod packed_option;
mod partition_slice;
mod predicates;
mod ref_slice;
mod simple_gvn;
mod topo_order;
mod write;

View File

@@ -0,0 +1,65 @@
//! A simple GVN pass.
use flowgraph::ControlFlowGraph;
use dominator_tree::DominatorTree;
use ir::{Cursor, InstructionData, Function, Inst, Opcode};
use std::collections::HashMap;
/// Test whether the given opcode is unsafe to even consider for GVN.
fn trivially_unsafe_for_gvn(opcode: Opcode) -> bool {
opcode.is_call() || opcode.is_branch() || opcode.is_terminator() || opcode.is_return() ||
opcode.can_trap()
}
/// Perform simple GVN on `func`.
///
pub fn do_simple_gvn(func: &mut Function, cfg: &mut ControlFlowGraph) {
let mut visible_values: HashMap<InstructionData, Inst> = HashMap::new();
let domtree = DominatorTree::with_function(func, &cfg);
// Visit EBBs in a reverse post-order.
let mut postorder = cfg.postorder_ebbs();
let mut pos = Cursor::new(&mut func.layout);
while let Some(ebb) = postorder.pop() {
pos.goto_top(ebb);
while let Some(inst) = pos.next_inst() {
let opcode = func.dfg[inst].opcode();
if trivially_unsafe_for_gvn(opcode) {
continue;
}
// TODO: Implement simple redundant-load elimination.
if opcode.can_store() {
continue;
}
if opcode.can_load() {
continue;
}
let key = func.dfg[inst].clone();
let entry = visible_values.entry(key);
use std::collections::hash_map::Entry::*;
match entry {
Occupied(mut entry) => {
if domtree.dominates(*entry.get(), inst, &pos.layout) {
func.dfg.replace_with_aliases(inst, *entry.get());
pos.remove_inst();
} else {
// The prior instruction doesn't dominate inst, so it
// won't dominate any subsequent instructions we'll
// visit, so just replace it.
*entry.get_mut() = inst;
continue;
}
}
Vacant(entry) => {
entry.insert(inst);
}
}
}
}
}

View File

@@ -20,6 +20,7 @@ mod legalizer;
mod regalloc;
mod runner;
mod runone;
mod simple_gvn;
mod verifier;
/// The result of running the test in a file.
@@ -62,6 +63,7 @@ fn new_subtest(parsed: &TestCommand) -> subtest::Result<Box<subtest::SubTest>> {
"legalizer" => legalizer::subtest(parsed),
"regalloc" => regalloc::subtest(parsed),
"binemit" => binemit::subtest(parsed),
"simple-gvn" => simple_gvn::subtest(parsed),
_ => Err(format!("unknown test command '{}'", parsed.command)),
}
}

View File

@@ -0,0 +1,51 @@
//! Test command for testing the simple GVN pass.
//!
//! The `simple-gvn` test command runs each function through the simple GVN pass after ensuring
//! that all instructions are legal for the target.
//!
//! The resulting function is sent to `filecheck`.
use cretonne::ir::Function;
use cretonne;
use cton_reader::TestCommand;
use filetest::subtest::{SubTest, Context, Result, run_filecheck};
use std::borrow::Cow;
use std::fmt::Write;
use utils::pretty_error;
struct TestSimpleGVN;
pub fn subtest(parsed: &TestCommand) -> Result<Box<SubTest>> {
assert_eq!(parsed.command, "simple-gvn");
if !parsed.options.is_empty() {
Err(format!("No options allowed on {}", parsed))
} else {
Ok(Box::new(TestSimpleGVN))
}
}
impl SubTest for TestSimpleGVN {
fn name(&self) -> Cow<str> {
Cow::from("simple-gvn")
}
fn is_mutating(&self) -> bool {
true
}
fn run(&self, func: Cow<Function>, context: &Context) -> Result<()> {
// Create a compilation context, and drop in the function.
let mut comp_ctx = cretonne::Context::new();
comp_ctx.func = func.into_owned();
comp_ctx.flowgraph();
comp_ctx
.simple_gvn()
.map_err(|e| pretty_error(&comp_ctx.func, e))?;
let mut text = String::new();
write!(&mut text, "{}", &comp_ctx.func)
.map_err(|e| e.to_string())?;
run_filecheck(&text, context)
}
}