peepmatic: Make the results of match operations a smaller and more cache friendly

This commit is contained in:
Nick Fitzgerald
2020-05-07 12:15:50 -07:00
parent 9a1f8038b7
commit 469104c4d3
14 changed files with 580 additions and 149 deletions

View File

@@ -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.

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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);
}
}

View File

@@ -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`

View File

@@ -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 {

View File

@@ -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;
};