peepmatic: Make the results of match operations a smaller and more cache friendly
This commit is contained in:
@@ -12,6 +12,8 @@ use std::fmt;
|
||||
#[repr(u32)]
|
||||
pub enum ConditionCode {
|
||||
/// Equal.
|
||||
// NB: We convert `ConditionCode` into `NonZeroU32`s with unchecked
|
||||
// conversions; memory safety relies on no variant being zero.
|
||||
Eq = 1,
|
||||
|
||||
/// Not equal.
|
||||
|
||||
@@ -21,7 +21,11 @@ use std::fmt::Debug;
|
||||
/// new `MachInst` and vcode backend easier, since all that needs to be done is
|
||||
/// "just" implementing this trait. (And probably add/modify some
|
||||
/// `peepmatic_runtime::operation::Operation`s as well).
|
||||
pub trait InstructionSet<'a> {
|
||||
///
|
||||
/// ## Safety
|
||||
///
|
||||
/// See doc comment for `instruction_result_bit_width`.
|
||||
pub unsafe trait InstructionSet<'a> {
|
||||
/// Mutable context passed into all trait methods. Can be whatever you want!
|
||||
///
|
||||
/// In practice, this is a `FuncCursor` for `cranelift-codegen`'s trait
|
||||
@@ -124,7 +128,10 @@ pub trait InstructionSet<'a> {
|
||||
|
||||
/// Get the bit width of the given instruction's result.
|
||||
///
|
||||
/// Must be one of 1, 8, 16, 32, 64, or 128.
|
||||
/// ## Safety
|
||||
///
|
||||
/// There is code that makes memory-safety assumptions that the result is
|
||||
/// always one of 1, 8, 16, 32, 64, or 128. Implementors must uphold this.
|
||||
fn instruction_result_bit_width(
|
||||
&self,
|
||||
context: &mut Self::Context,
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryInto;
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
/// An identifier for an interned integer.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct IntegerId(#[doc(hidden)] pub u32);
|
||||
pub struct IntegerId(#[doc(hidden)] pub NonZeroU32);
|
||||
|
||||
/// An interner for integer values.
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
@@ -40,7 +40,8 @@ impl IntegerInterner {
|
||||
return *id;
|
||||
}
|
||||
|
||||
let id = IntegerId(self.values.len().try_into().unwrap());
|
||||
assert!((self.values.len() as u64) < (std::u32::MAX as u64));
|
||||
let id = IntegerId(unsafe { NonZeroU32::new_unchecked(self.values.len() as u32 + 1) });
|
||||
|
||||
self.values.push(value);
|
||||
self.map.insert(value, id);
|
||||
@@ -59,13 +60,21 @@ impl IntegerInterner {
|
||||
/// Lookup a previously interned integer by id.
|
||||
#[inline]
|
||||
pub fn lookup(&self, id: IntegerId) -> u64 {
|
||||
self.values[id.0 as usize]
|
||||
let index = id.0.get() as usize - 1;
|
||||
self.values[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IntegerId> for u32 {
|
||||
#[inline]
|
||||
fn from(id: IntegerId) -> u32 {
|
||||
id.0.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IntegerId> for NonZeroU32 {
|
||||
#[inline]
|
||||
fn from(id: IntegerId) -> NonZeroU32 {
|
||||
id.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::operator::{Operator, UnquoteOperator};
|
||||
use crate::paths::{PathId, PathInterner};
|
||||
use crate::r#type::{BitWidth, Type};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
/// A set of linear optimizations.
|
||||
#[derive(Debug)]
|
||||
@@ -32,6 +33,26 @@ pub struct Optimization {
|
||||
pub increments: Vec<Increment>,
|
||||
}
|
||||
|
||||
/// Match any value.
|
||||
///
|
||||
/// This can be used to create fallback, wildcard-style transitions between
|
||||
/// states.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct Else;
|
||||
|
||||
/// The result of evaluating a `MatchOp`.
|
||||
///
|
||||
/// This is either a specific non-zero `u32`, or a fallback that matches
|
||||
/// everything.
|
||||
pub type MatchResult = Result<NonZeroU32, Else>;
|
||||
|
||||
/// Convert a boolean to a `MatchResult`.
|
||||
#[inline]
|
||||
pub fn bool_to_match_result(b: bool) -> MatchResult {
|
||||
let b = b as u32;
|
||||
unsafe { Ok(NonZeroU32::new_unchecked(b + 1)) }
|
||||
}
|
||||
|
||||
/// An increment is a matching operation, the expected result from that
|
||||
/// operation to continue to the next increment, and the actions to take to
|
||||
/// build up the LHS scope and RHS instructions given that we got the expected
|
||||
@@ -44,9 +65,9 @@ pub struct Increment {
|
||||
pub operation: MatchOp,
|
||||
|
||||
/// The expected result of our matching operation, that enables us to
|
||||
/// continue to the next increment. `None` is used for wildcard-style "else"
|
||||
/// transitions.
|
||||
pub expected: Option<u32>,
|
||||
/// continue to the next increment, or `Else` for "don't care"
|
||||
/// wildcard-style matching.
|
||||
pub expected: MatchResult,
|
||||
|
||||
/// Actions to perform, given that the operation resulted in the expected
|
||||
/// value.
|
||||
@@ -217,3 +238,23 @@ pub enum Action {
|
||||
operands: [RhsId; 3],
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn match_result_is_4_bytes_in_size() {
|
||||
assert_eq!(std::mem::size_of::<MatchResult>(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_op_is_12_bytes_in_size() {
|
||||
assert_eq!(std::mem::size_of::<MatchOp>(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn action_is_20_bytes_in_size() {
|
||||
assert_eq!(std::mem::size_of::<Action>(), 20);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ use serde::{Deserialize, Serialize};
|
||||
pub enum Operator {
|
||||
/// `adjust_sp_down`
|
||||
#[peepmatic(params(iNN), result(void))]
|
||||
// NB: We convert `Operator`s into `NonZeroU32`s with unchecked casts;
|
||||
// memory safety relies on `Operator` starting at `1` and no variant ever
|
||||
// being zero.
|
||||
AdjustSpDown = 1,
|
||||
|
||||
/// `adjust_sp_down_imm`
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use crate::error::Result;
|
||||
use crate::instruction_set::InstructionSet;
|
||||
use crate::integer_interner::IntegerInterner;
|
||||
use crate::linear::{Action, MatchOp};
|
||||
use crate::linear::{Action, MatchOp, MatchResult};
|
||||
use crate::optimizer::PeepholeOptimizer;
|
||||
use crate::paths::PathInterner;
|
||||
use peepmatic_automata::Automaton;
|
||||
@@ -29,7 +29,7 @@ pub struct PeepholeOptimizations {
|
||||
|
||||
/// The underlying automata for matching optimizations' left-hand sides, and
|
||||
/// building up the corresponding right-hand side.
|
||||
pub automata: Automaton<Option<u32>, MatchOp, Vec<Action>>,
|
||||
pub automata: Automaton<MatchResult, MatchOp, Vec<Action>>,
|
||||
}
|
||||
|
||||
impl PeepholeOptimizations {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! An optimizer for a set of peephole optimizations.
|
||||
|
||||
use crate::instruction_set::InstructionSet;
|
||||
use crate::linear::{Action, MatchOp};
|
||||
use crate::linear::{bool_to_match_result, Action, Else, MatchOp, MatchResult};
|
||||
use crate::operator::UnquoteOperator;
|
||||
use crate::optimizations::PeepholeOptimizations;
|
||||
use crate::part::{Constant, Part};
|
||||
@@ -10,6 +10,7 @@ use peepmatic_automata::State;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::{self, Debug};
|
||||
use std::mem;
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
/// A peephole optimizer instance that can apply a set of peephole
|
||||
/// optimizations to instructions.
|
||||
@@ -275,43 +276,72 @@ where
|
||||
context: &mut I::Context,
|
||||
root: I::Instruction,
|
||||
match_op: MatchOp,
|
||||
) -> Option<u32> {
|
||||
) -> MatchResult {
|
||||
use crate::linear::MatchOp::*;
|
||||
|
||||
log::trace!("Evaluating match operation: {:?}", match_op);
|
||||
let result = match match_op {
|
||||
let result: MatchResult = (|| match match_op {
|
||||
Opcode { path } => {
|
||||
let path = self.peep_opt.paths.lookup(path);
|
||||
let part = self.instr_set.get_part_at_path(context, root, path)?;
|
||||
let inst = part.as_instruction()?;
|
||||
self.instr_set.operator(context, inst).map(|op| op as u32)
|
||||
let part = self
|
||||
.instr_set
|
||||
.get_part_at_path(context, root, path)
|
||||
.ok_or(Else)?;
|
||||
let inst = part.as_instruction().ok_or(Else)?;
|
||||
let op = self.instr_set.operator(context, inst).ok_or(Else)?;
|
||||
let op = op as u32;
|
||||
debug_assert!(
|
||||
op != 0,
|
||||
"`Operator` doesn't have any variant represented
|
||||
with zero"
|
||||
);
|
||||
Ok(unsafe { NonZeroU32::new_unchecked(op as u32) })
|
||||
}
|
||||
IsConst { path } => {
|
||||
let path = self.peep_opt.paths.lookup(path);
|
||||
let part = self.instr_set.get_part_at_path(context, root, path)?;
|
||||
let part = self
|
||||
.instr_set
|
||||
.get_part_at_path(context, root, path)
|
||||
.ok_or(Else)?;
|
||||
let is_const = match part {
|
||||
Part::Instruction(i) => {
|
||||
self.instr_set.instruction_to_constant(context, i).is_some()
|
||||
}
|
||||
Part::ConditionCode(_) | Part::Constant(_) => true,
|
||||
};
|
||||
Some(is_const as u32)
|
||||
bool_to_match_result(is_const)
|
||||
}
|
||||
IsPowerOfTwo { path } => {
|
||||
let path = self.peep_opt.paths.lookup(path);
|
||||
let part = self.instr_set.get_part_at_path(context, root, path)?;
|
||||
let part = self
|
||||
.instr_set
|
||||
.get_part_at_path(context, root, path)
|
||||
.ok_or(Else)?;
|
||||
match part {
|
||||
Part::Constant(c) => Some(c.as_int().unwrap().is_power_of_two() as u32),
|
||||
Part::Instruction(i) => {
|
||||
let c = self.instr_set.instruction_to_constant(context, i)?;
|
||||
Some(c.as_int().unwrap().is_power_of_two() as u32)
|
||||
Part::Constant(c) => {
|
||||
let is_pow2 = c.as_int().unwrap().is_power_of_two();
|
||||
bool_to_match_result(is_pow2)
|
||||
}
|
||||
Part::ConditionCode(_) => panic!("IsPowerOfTwo on a condition code"),
|
||||
Part::Instruction(i) => {
|
||||
let c = self
|
||||
.instr_set
|
||||
.instruction_to_constant(context, i)
|
||||
.ok_or(Else)?;
|
||||
let is_pow2 = c.as_int().unwrap().is_power_of_two();
|
||||
bool_to_match_result(is_pow2)
|
||||
}
|
||||
Part::ConditionCode(_) => unreachable!(
|
||||
"IsPowerOfTwo on a condition
|
||||
code"
|
||||
),
|
||||
}
|
||||
}
|
||||
BitWidth { path } => {
|
||||
let path = self.peep_opt.paths.lookup(path);
|
||||
let part = self.instr_set.get_part_at_path(context, root, path)?;
|
||||
let part = self
|
||||
.instr_set
|
||||
.get_part_at_path(context, root, path)
|
||||
.ok_or(Else)?;
|
||||
let bit_width = match part {
|
||||
Part::Instruction(i) => self.instr_set.instruction_result_bit_width(context, i),
|
||||
Part::Constant(Constant::Int(_, w)) | Part::Constant(Constant::Bool(_, w)) => {
|
||||
@@ -321,14 +351,22 @@ where
|
||||
}
|
||||
Part::ConditionCode(_) => panic!("BitWidth on condition code"),
|
||||
};
|
||||
Some(bit_width as u32)
|
||||
debug_assert!(
|
||||
bit_width != 0,
|
||||
"`InstructionSet` implementors must uphold the contract that \
|
||||
`instruction_result_bit_width` returns one of 1, 8, 16, 32, 64, or 128"
|
||||
);
|
||||
Ok(unsafe { NonZeroU32::new_unchecked(bit_width as u32) })
|
||||
}
|
||||
FitsInNativeWord { path } => {
|
||||
let native_word_size = self.instr_set.native_word_size_in_bits(context);
|
||||
debug_assert!(native_word_size.is_power_of_two());
|
||||
|
||||
let path = self.peep_opt.paths.lookup(path);
|
||||
let part = self.instr_set.get_part_at_path(context, root, path)?;
|
||||
let part = self
|
||||
.instr_set
|
||||
.get_part_at_path(context, root, path)
|
||||
.ok_or(Else)?;
|
||||
let fits = match part {
|
||||
Part::Instruction(i) => {
|
||||
let size = self.instr_set.instruction_result_bit_width(context, i);
|
||||
@@ -341,13 +379,19 @@ where
|
||||
}
|
||||
Part::ConditionCode(_) => panic!("FitsInNativeWord on condition code"),
|
||||
};
|
||||
Some(fits as u32)
|
||||
bool_to_match_result(fits)
|
||||
}
|
||||
Eq { path_a, path_b } => {
|
||||
let path_a = self.peep_opt.paths.lookup(path_a);
|
||||
let part_a = self.instr_set.get_part_at_path(context, root, path_a)?;
|
||||
let part_a = self
|
||||
.instr_set
|
||||
.get_part_at_path(context, root, path_a)
|
||||
.ok_or(Else)?;
|
||||
let path_b = self.peep_opt.paths.lookup(path_b);
|
||||
let part_b = self.instr_set.get_part_at_path(context, root, path_b)?;
|
||||
let part_b = self
|
||||
.instr_set
|
||||
.get_part_at_path(context, root, path_b)
|
||||
.ok_or(Else)?;
|
||||
let eq = match (part_a, part_b) {
|
||||
(Part::Instruction(inst), Part::Constant(c1))
|
||||
| (Part::Constant(c1), Part::Instruction(inst)) => {
|
||||
@@ -358,43 +402,67 @@ where
|
||||
}
|
||||
(a, b) => a == b,
|
||||
};
|
||||
Some(eq as _)
|
||||
bool_to_match_result(eq)
|
||||
}
|
||||
IntegerValue { path } => {
|
||||
let path = self.peep_opt.paths.lookup(path);
|
||||
let part = self.instr_set.get_part_at_path(context, root, path)?;
|
||||
let part = self
|
||||
.instr_set
|
||||
.get_part_at_path(context, root, path)
|
||||
.ok_or(Else)?;
|
||||
match part {
|
||||
Part::Constant(c) => {
|
||||
let x = c.as_int()?;
|
||||
self.peep_opt.integers.already_interned(x).map(|id| id.0)
|
||||
let x = c.as_int().ok_or(Else)?;
|
||||
let id = self.peep_opt.integers.already_interned(x).ok_or(Else)?;
|
||||
Ok(id.0)
|
||||
}
|
||||
Part::Instruction(i) => {
|
||||
let c = self.instr_set.instruction_to_constant(context, i)?;
|
||||
let x = c.as_int()?;
|
||||
self.peep_opt.integers.already_interned(x).map(|id| id.0)
|
||||
let c = self
|
||||
.instr_set
|
||||
.instruction_to_constant(context, i)
|
||||
.ok_or(Else)?;
|
||||
let x = c.as_int().ok_or(Else)?;
|
||||
let id = self.peep_opt.integers.already_interned(x).ok_or(Else)?;
|
||||
Ok(id.0)
|
||||
}
|
||||
Part::ConditionCode(_) => panic!("IntegerValue on condition code"),
|
||||
Part::ConditionCode(_) => unreachable!("IntegerValue on condition code"),
|
||||
}
|
||||
}
|
||||
BooleanValue { path } => {
|
||||
let path = self.peep_opt.paths.lookup(path);
|
||||
let part = self.instr_set.get_part_at_path(context, root, path)?;
|
||||
let part = self
|
||||
.instr_set
|
||||
.get_part_at_path(context, root, path)
|
||||
.ok_or(Else)?;
|
||||
match part {
|
||||
Part::Constant(c) => c.as_bool().map(|b| b as u32),
|
||||
Part::Instruction(i) => {
|
||||
let c = self.instr_set.instruction_to_constant(context, i)?;
|
||||
c.as_bool().map(|b| b as u32)
|
||||
Part::Constant(c) => {
|
||||
let b = c.as_bool().ok_or(Else)?;
|
||||
bool_to_match_result(b)
|
||||
}
|
||||
Part::ConditionCode(_) => panic!("IntegerValue on condition code"),
|
||||
Part::Instruction(i) => {
|
||||
let c = self
|
||||
.instr_set
|
||||
.instruction_to_constant(context, i)
|
||||
.ok_or(Else)?;
|
||||
let b = c.as_bool().ok_or(Else)?;
|
||||
bool_to_match_result(b)
|
||||
}
|
||||
Part::ConditionCode(_) => unreachable!("IntegerValue on condition code"),
|
||||
}
|
||||
}
|
||||
ConditionCode { path } => {
|
||||
let path = self.peep_opt.paths.lookup(path);
|
||||
let part = self.instr_set.get_part_at_path(context, root, path)?;
|
||||
part.as_condition_code().map(|cc| cc as u32)
|
||||
let part = self
|
||||
.instr_set
|
||||
.get_part_at_path(context, root, path)
|
||||
.ok_or(Else)?;
|
||||
let cc = part.as_condition_code().ok_or(Else)?;
|
||||
let cc = cc as u32;
|
||||
debug_assert!(cc != 0);
|
||||
Ok(unsafe { NonZeroU32::new_unchecked(cc) })
|
||||
}
|
||||
MatchOp::Nop => None,
|
||||
};
|
||||
MatchOp::Nop => Err(Else),
|
||||
})();
|
||||
log::trace!("Evaluated match operation: {:?} = {:?}", match_op, result);
|
||||
result
|
||||
}
|
||||
@@ -437,12 +505,12 @@ where
|
||||
r#final = Some((query.current_state(), self.actions.len()));
|
||||
}
|
||||
|
||||
// Anything following a `None` transition doesn't care about the
|
||||
// Anything following a `Else` transition doesn't care about the
|
||||
// result of this match operation, so if we partially follow the
|
||||
// current non-`None` path, but don't ultimately find a matching
|
||||
// current non-`Else` path, but don't ultimately find a matching
|
||||
// optimization, we want to be able to backtrack to this state and
|
||||
// then try taking the `None` transition.
|
||||
if query.has_transition_on(&None) {
|
||||
// then try taking the `Else` transition.
|
||||
if query.has_transition_on(&Err(Else)) {
|
||||
self.backtracking_states
|
||||
.push((query.current_state(), self.actions.len()));
|
||||
}
|
||||
@@ -462,8 +530,8 @@ where
|
||||
query.go_to_state(state);
|
||||
self.actions.truncate(actions_len);
|
||||
query
|
||||
.next(&None)
|
||||
.expect("backtracking states always have `None` transitions")
|
||||
.next(&Err(Else))
|
||||
.expect("backtracking states always have `Else` transitions")
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user