1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
* Iterator

* Use the new iterator for the X86 backend split

* Use iterator for reg alloc, remove forward pass

* Fix up iterator usage on AArch64

* Update yjit/src/backend/ir.rs

Co-authored-by: Maxime Chevalier-Boisvert <maximechevalierb@gmail.com>

* Various PR feedback for iterators for IR

* Use a local mutable reference for a64_split

* Move tests from ir.rs to tests.rs in backend

* Fix x86 shift instructions live range calculation

* Iterator

* Use the new iterator for the X86 backend split

* Fix up x86 iterator usage

* Fix ARM iterator usage

* Remove unintentionally duplicated tests
This commit is contained in:
Kevin Newton 2022-08-04 15:29:31 -04:00 committed by Takashi Kokubun
parent 49c9f893f8
commit 3f42028e3e
No known key found for this signature in database
GPG key ID: 6FFC433B12EE23DD
4 changed files with 273 additions and 149 deletions

View file

@ -182,12 +182,14 @@ impl Assembler
}
}
self.forward_pass(|asm, index, op, opnds, target, text, pos_marker, original_opnds| {
// Load all Value operands into registers that aren't already a part
// of Load instructions.
let opnds = match op {
Op::Load => opnds,
_ => opnds.into_iter().map(|opnd| {
let mut asm_local = Assembler::new_with_label_names(std::mem::take(&mut self.label_names));
let asm = &mut asm_local;
let mut iterator = self.into_draining_iter();
while let Some((index, insn)) = iterator.next_mapped() {
let opnds = match insn.op {
Op::Load => insn.opnds,
_ => insn.opnds.into_iter().map(|opnd| {
if let Opnd::Value(_) = opnd {
asm.load(opnd)
} else {
@ -196,7 +198,7 @@ impl Assembler
}).collect()
};
match op {
match insn.op {
Op::Add => {
match (opnds[0], opnds[1]) {
(Opnd::Reg(_) | Opnd::InsnOut { .. }, Opnd::Reg(_) | Opnd::InsnOut { .. }) => {
@ -217,17 +219,17 @@ impl Assembler
Op::And | Op::Or => {
match (opnds[0], opnds[1]) {
(Opnd::Reg(_), Opnd::Reg(_)) => {
asm.push_insn(op, vec![opnds[0], opnds[1]], target, text, pos_marker);
asm.push_insn(insn.op, vec![opnds[0], opnds[1]], insn.target, insn.text, insn.pos_marker);
},
(reg_opnd @ Opnd::Reg(_), other_opnd) |
(other_opnd, reg_opnd @ Opnd::Reg(_)) => {
let opnd1 = split_bitmask_immediate(asm, other_opnd);
asm.push_insn(op, vec![reg_opnd, opnd1], target, text, pos_marker);
asm.push_insn(insn.op, vec![reg_opnd, opnd1], insn.target, insn.text, insn.pos_marker);
},
_ => {
let opnd0 = split_load_operand(asm, opnds[0]);
let opnd1 = split_bitmask_immediate(asm, opnds[1]);
asm.push_insn(op, vec![opnd0, opnd1], target, text, pos_marker);
asm.push_insn(insn.op, vec![opnd0, opnd1], insn.target, insn.text, insn.pos_marker);
}
}
},
@ -246,7 +248,7 @@ impl Assembler
// Now we push the CCall without any arguments so that it
// just performs the call.
asm.ccall(target.unwrap().unwrap_fun_ptr(), vec![]);
asm.ccall(insn.target.unwrap().unwrap_fun_ptr(), vec![]);
},
Op::Cmp => {
let opnd0 = match opnds[0] {
@ -273,7 +275,7 @@ impl Assembler
}
}).collect();
asm.push_insn(op, new_opnds, target, text, pos_marker);
asm.push_insn(insn.op, new_opnds, insn.target, insn.text, insn.pos_marker);
},
Op::IncrCounter => {
// We'll use LDADD later which only works with registers
@ -392,10 +394,14 @@ impl Assembler
asm.test(opnd0, opnd1);
},
_ => {
asm.push_insn(op, opnds, target, text, pos_marker);
asm.push_insn(insn.op, opnds, insn.target, insn.text, insn.pos_marker);
}
};
})
iterator.map_insn_index(asm);
}
asm_local
}
/// Emit platform-specific machine code

View file

@ -2,8 +2,10 @@
#![allow(unused_variables)]
#![allow(unused_imports)]
use std::cell::Cell;
use std::fmt;
use std::convert::From;
use std::mem::take;
use crate::cruby::{VALUE};
use crate::virtualmem::{CodePtr};
use crate::asm::{CodeBlock, uimm_num_bits, imm_num_bits};
@ -288,6 +290,20 @@ impl Opnd
_ => unreachable!()
}
}
/// Maps the indices from a previous list of instructions to a new list of
/// instructions.
pub fn map_index(self, indices: &Vec<usize>) -> Opnd {
match self {
Opnd::InsnOut { idx, num_bits } => {
Opnd::InsnOut { idx: indices[idx], num_bits }
}
Opnd::Mem(Mem { base: MemBase::InsnOut(idx), disp, num_bits }) => {
Opnd::Mem(Mem { base: MemBase::InsnOut(indices[idx]), disp, num_bits })
},
_ => self
}
}
}
impl From<usize> for Opnd {
@ -433,11 +449,15 @@ pub struct Assembler
impl Assembler
{
pub fn new() -> Assembler {
Assembler {
pub fn new() -> Self {
Self::new_with_label_names(Vec::default())
}
pub fn new_with_label_names(label_names: Vec<String>) -> Self {
Self {
insns: Vec::default(),
live_ranges: Vec::default(),
label_names: Vec::default(),
label_names
}
}
@ -573,58 +593,6 @@ impl Assembler
self.live_ranges.push(self.insns.len());
}
/// Transform input instructions, consumes the input assembler
pub(super) fn forward_pass<F>(mut self, mut map_insn: F) -> Assembler
where F: FnMut(&mut Assembler, usize, Op, Vec<Opnd>, Option<Target>, Option<String>, Option<PosMarkerFn>, Vec<Opnd>)
{
let mut asm = Assembler {
insns: Vec::default(),
live_ranges: Vec::default(),
label_names: self.label_names,
};
// Indices maps from the old instruction index to the new instruction
// index.
let mut indices: Vec<usize> = Vec::default();
// Map an operand to the next set of instructions by correcting previous
// InsnOut indices.
fn map_opnd(opnd: Opnd, indices: &mut Vec<usize>) -> Opnd {
match opnd {
Opnd::InsnOut{ idx, num_bits } => {
Opnd::InsnOut{ idx: indices[idx], num_bits }
}
Opnd::Mem(Mem{ base: MemBase::InsnOut(idx), disp, num_bits, }) => {
Opnd::Mem(Mem{ base:MemBase::InsnOut(indices[idx]), disp, num_bits })
}
_ => opnd
}
}
for (index, insn) in self.insns.drain(..).enumerate() {
let original_opnds = insn.opnds.clone();
let opnds: Vec<Opnd> = insn.opnds.into_iter().map(|opnd| map_opnd(opnd, &mut indices)).collect();
// For each instruction, either handle it here or allow the map_insn
// callback to handle it.
match insn.op {
Op::Comment => {
asm.comment(insn.text.unwrap().as_str());
},
_ => {
map_insn(&mut asm, index, insn.op, opnds, insn.target, insn.text, insn.pos_marker, original_opnds);
}
};
// Here we're assuming that if we've pushed multiple instructions,
// the output that we're using is still the final instruction that
// was pushed.
indices.push(asm.insns.len() - 1);
}
asm
}
/// Sets the out field on the various instructions that require allocated
/// registers because their output is used as the operand on a subsequent
/// instruction. This is our implementation of the linear scan algorithm.
@ -671,13 +639,15 @@ impl Assembler
}
}
let live_ranges: Vec<usize> = std::mem::take(&mut self.live_ranges);
let live_ranges: Vec<usize> = take(&mut self.live_ranges);
let mut asm = Assembler::new_with_label_names(take(&mut self.label_names));
let mut iterator = self.into_draining_iter();
let asm = self.forward_pass(|asm, index, op, opnds, target, text, pos_marker, original_insns| {
while let Some((index, insn)) = iterator.next_unmapped() {
// Check if this is the last instruction that uses an operand that
// spans more than one instruction. In that case, return the
// allocated register to the pool.
for opnd in &opnds {
for opnd in &insn.opnds {
match opnd {
Opnd::InsnOut{idx, .. } |
Opnd::Mem( Mem { base: MemBase::InsnOut(idx), .. }) => {
@ -693,7 +663,7 @@ impl Assembler
if let Opnd::Reg(reg) = asm.insns[start_index].out {
dealloc_reg(&mut pool, &regs, &reg);
} else {
unreachable!("no register allocated for insn {:?}", op);
unreachable!("no register allocated for insn {:?}", insn.op);
}
}
}
@ -703,7 +673,7 @@ impl Assembler
}
// C return values need to be mapped to the C return register
if op == Op::CCall {
if insn.op == Op::CCall {
assert_eq!(pool, 0, "register lives past C function call");
}
@ -713,7 +683,7 @@ impl Assembler
if live_ranges[index] != index {
// C return values need to be mapped to the C return register
if op == Op::CCall {
if insn.op == Op::CCall {
out_reg = Opnd::Reg(take_reg(&mut pool, &regs, &C_RET_REG))
}
@ -722,8 +692,8 @@ impl Assembler
// We do this to improve register allocation on x86
// e.g. out = add(reg0, reg1)
// reg0 = add(reg0, reg1)
else if opnds.len() > 0 {
if let Opnd::InsnOut{idx, ..} = opnds[0] {
else if insn.opnds.len() > 0 {
if let Opnd::InsnOut{idx, ..} = insn.opnds[0] {
if live_ranges[idx] == index {
if let Opnd::Reg(reg) = asm.insns[idx].out {
out_reg = Opnd::Reg(take_reg(&mut pool, &regs, &reg))
@ -734,9 +704,9 @@ impl Assembler
// Allocate a new register for this instruction
if out_reg == Opnd::None {
out_reg = if op == Op::LiveReg {
out_reg = if insn.op == Op::LiveReg {
// Allocate a specific register
let reg = opnds[0].unwrap_reg();
let reg = insn.opnds[0].unwrap_reg();
Opnd::Reg(take_reg(&mut pool, &regs, &reg))
} else {
Opnd::Reg(alloc_reg(&mut pool, &regs))
@ -745,7 +715,7 @@ impl Assembler
}
// Replace InsnOut operands by their corresponding register
let reg_opnds: Vec<Opnd> = opnds.into_iter().map(|opnd|
let reg_opnds: Vec<Opnd> = insn.opnds.into_iter().map(|opnd|
match opnd {
Opnd::InsnOut{idx, ..} => asm.insns[idx].out,
Opnd::Mem(Mem { base: MemBase::InsnOut(idx), disp, num_bits }) => {
@ -760,7 +730,7 @@ impl Assembler
}
).collect();
asm.push_insn(op, reg_opnds, target, text, pos_marker);
asm.push_insn(insn.op, reg_opnds, insn.target, insn.text, insn.pos_marker);
// Set the output register for this instruction
let num_insns = asm.insns.len();
@ -770,7 +740,7 @@ impl Assembler
out_reg = Opnd::Reg(reg.sub_reg(num_out_bits))
}
new_insn.out = out_reg;
});
}
assert_eq!(pool, 0, "Expected all registers to be returned to the pool");
asm
@ -792,6 +762,123 @@ impl Assembler
let alloc_regs = alloc_regs.drain(0..num_regs).collect();
self.compile_with_regs(cb, alloc_regs)
}
/// Consume the assembler by creating a new draining iterator.
pub fn into_draining_iter(self) -> AssemblerDrainingIterator {
AssemblerDrainingIterator::new(self)
}
/// Consume the assembler by creating a new lookback iterator.
pub fn into_lookback_iter(self) -> AssemblerLookbackIterator {
AssemblerLookbackIterator::new(self)
}
pub fn ccall(&mut self, fptr: *const u8, opnds: Vec<Opnd>) -> Opnd {
let target = Target::FunPtr(fptr);
self.push_insn(Op::CCall, opnds, Some(target), None, None)
}
// pub fn pos_marker<F: FnMut(CodePtr)>(&mut self, marker_fn: F)
pub fn pos_marker(&mut self, marker_fn: impl Fn(CodePtr) + 'static) {
self.push_insn(Op::PosMarker, vec![], None, None, Some(Box::new(marker_fn)));
}
}
/// A struct that allows iterating through an assembler's instructions and
/// consuming them as it iterates.
pub struct AssemblerDrainingIterator {
insns: std::vec::IntoIter<Insn>,
index: usize,
indices: Vec<usize>
}
impl AssemblerDrainingIterator {
fn new(asm: Assembler) -> Self {
Self {
insns: asm.insns.into_iter(),
index: 0,
indices: Vec::default()
}
}
/// When you're working with two lists of instructions, you need to make
/// sure you do some bookkeeping to align the indices contained within the
/// operands of the two lists.
///
/// This function accepts the assembler that is being built and tracks the
/// end of the current list of instructions in order to maintain that
/// alignment.
pub fn map_insn_index(&mut self, asm: &mut Assembler) {
self.indices.push(asm.insns.len() - 1);
}
/// Map an operand by using this iterator's list of mapped indices.
pub fn map_opnd(&self, opnd: Opnd) -> Opnd {
opnd.map_index(&self.indices)
}
/// Returns the next instruction in the list with the indices corresponding
/// to the next list of instructions.
pub fn next_mapped(&mut self) -> Option<(usize, Insn)> {
self.next_unmapped().map(|(index, insn)| {
let opnds = insn.opnds.into_iter().map(|opnd| opnd.map_index(&self.indices)).collect();
(index, Insn { opnds, ..insn })
})
}
/// Returns the next instruction in the list with the indices corresponding
/// to the previous list of instructions.
pub fn next_unmapped(&mut self) -> Option<(usize, Insn)> {
let index = self.index;
self.index += 1;
self.insns.next().map(|insn| (index, insn))
}
}
/// A struct that allows iterating through references to an assembler's
/// instructions without consuming them.
pub struct AssemblerLookbackIterator {
asm: Assembler,
index: Cell<usize>
}
impl AssemblerLookbackIterator {
fn new(asm: Assembler) -> Self {
Self { asm, index: Cell::new(0) }
}
/// Fetches a reference to an instruction at a specific index.
pub fn get(&self, index: usize) -> Option<&Insn> {
self.asm.insns.get(index)
}
/// Fetches a reference to an instruction in the list relative to the
/// current cursor location of this iterator.
pub fn get_relative(&self, difference: i32) -> Option<&Insn> {
let index: Result<i32, _> = self.index.get().try_into();
let relative: Result<usize, _> = index.and_then(|value| (value + difference).try_into());
relative.ok().and_then(|value| self.asm.insns.get(value))
}
/// Fetches the previous instruction relative to the current cursor location
/// of this iterator.
pub fn get_previous(&self) -> Option<&Insn> {
self.get_relative(-1)
}
/// Fetches the next instruction relative to the current cursor location of
/// this iterator.
pub fn get_next(&self) -> Option<&Insn> {
self.get_relative(1)
}
/// Returns the next instruction in the list with the indices corresponding
/// to the previous list of instructions.
pub fn next_unmapped(&self) -> Option<(usize, &Insn)> {
let index = self.index.get();
self.index.set(index + 1);
self.asm.insns.get(index).map(|insn| (index, insn))
}
}
impl fmt::Debug for Assembler {
@ -806,21 +893,6 @@ impl fmt::Debug for Assembler {
}
}
impl Assembler
{
pub fn ccall(&mut self, fptr: *const u8, opnds: Vec<Opnd>) -> Opnd
{
let target = Target::FunPtr(fptr);
self.push_insn(Op::CCall, opnds, Some(target), None, None)
}
//pub fn pos_marker<F: FnMut(CodePtr)>(&mut self, marker_fn: F)
pub fn pos_marker(&mut self, marker_fn: impl Fn(CodePtr) + 'static)
{
self.push_insn(Op::PosMarker, vec![], None, None, Some(Box::new(marker_fn)));
}
}
macro_rules! def_push_jcc {
($op_name:ident, $opcode:expr) => {
impl Assembler

View file

@ -299,3 +299,41 @@ fn test_bake_string() {
asm.bake_string("Hello, world!");
asm.compile_with_num_regs(&mut cb, 0);
}
#[test]
fn test_draining_iterator() {
let mut asm = Assembler::new();
asm.load(Opnd::None);
asm.store(Opnd::None, Opnd::None);
asm.add(Opnd::None, Opnd::None);
let mut iter = asm.into_draining_iter();
while let Some((index, insn)) = iter.next_unmapped() {
match index {
0 => assert_eq!(insn.op, Op::Load),
1 => assert_eq!(insn.op, Op::Store),
2 => assert_eq!(insn.op, Op::Add),
_ => panic!("Unexpected instruction index"),
};
}
}
#[test]
fn test_lookback_iterator() {
let mut asm = Assembler::new();
asm.load(Opnd::None);
asm.store(Opnd::None, Opnd::None);
asm.store(Opnd::None, Opnd::None);
let mut iter = asm.into_lookback_iter();
while let Some((index, insn)) = iter.next_unmapped() {
if index > 0 {
assert_eq!(iter.get_previous().unwrap().opnds[0], Opnd::None);
assert_eq!(insn.op, Op::Store);
}
}
}

View file

@ -2,11 +2,13 @@
#![allow(unused_variables)]
#![allow(unused_imports)]
use std::mem::take;
use crate::asm::*;
use crate::asm::x86_64::*;
use crate::codegen::{JITState};
use crate::cruby::*;
use crate::backend::ir::{Assembler, Opnd, Target, Op, MemBase, Mem};
use crate::backend::ir::*;
// Use the x86 register type for this platform
pub type Reg = X86Reg;
@ -94,31 +96,51 @@ impl Assembler
/// Split IR instructions for the x86 platform
fn x86_split(mut self) -> Assembler
{
let live_ranges: Vec<usize> = std::mem::take(&mut self.live_ranges);
let live_ranges: Vec<usize> = take(&mut self.live_ranges);
let mut asm = Assembler::new_with_label_names(take(&mut self.label_names));
let mut iterator = self.into_draining_iter();
self.forward_pass(|asm, index, op, opnds, target, text, pos_marker, original_opnds| {
// Load VALUEs into registers because
// - Most instructions can't be encoded with 64-bit immediates.
// - We look for Op::Load specifically when emiting to keep GC'ed
// VALUEs alive. This is a sort of canonicalization.
let opnds = match op {
Op::Load => opnds,
_ => opnds.into_iter().map(|opnd| {
if let Opnd::Value(value) = opnd {
// Since mov(mem64, imm32) sign extends, as_i64() makes sure we split
// when the extended value is different.
if !value.special_const_p() || imm_num_bits(value.as_i64()) > 32 {
return asm.load(opnd);
}
while let Some((index, insn)) = iterator.next_unmapped() {
// When we're iterating through the instructions with x86_split, we
// need to know the previous live ranges in order to tell if a
// register lasts beyond the current instruction. So instead of
// using next_mapped, we call next_unmapped. When you're using the
// next_unmapped API, you need to make sure that you map each
// operand that could reference an old index, which means both
// Opnd::InsnOut operands and Opnd::Mem operands with a base of
// MemBase::InsnOut.
//
// You need to ensure that you only map it _once_, because otherwise
// you'll end up mapping an incorrect index which could end up being
// out of bounds of the old set of indices.
//
// We handle all of that mapping here to ensure that it's only
// mapped once. We also handle loading Opnd::Value operands into
// registers here so that all mapping happens in one place. We load
// Opnd::Value operands into registers here because:
//
// - Most instructions can't be encoded with 64-bit immediates.
// - We look for Op::Load specifically when emiting to keep GC'ed
// VALUEs alive. This is a sort of canonicalization.
let opnds: Vec<Opnd> = insn.opnds.iter().map(|opnd| {
if insn.op == Op::Load {
iterator.map_opnd(*opnd)
} else if let Opnd::Value(value) = opnd {
// Since mov(mem64, imm32) sign extends, as_i64() makes sure
// we split when the extended value is different.
if !value.special_const_p() || imm_num_bits(value.as_i64()) > 32 {
asm.load(iterator.map_opnd(*opnd))
} else {
iterator.map_opnd(*opnd)
}
} else {
iterator.map_opnd(*opnd)
}
}).collect();
opnd
}).collect()
};
match op {
match insn.op {
Op::Add | Op::Sub | Op::And | Op::Cmp | Op::Or | Op::Test => {
let (opnd0, opnd1) = match (opnds[0], opnds[1]) {
let (opnd0, opnd1) = match (insn.opnds[0], insn.opnds[1]) {
(Opnd::Mem(_), Opnd::Mem(_)) => {
(asm.load(opnds[0]), asm.load(opnds[1]))
},
@ -138,17 +160,7 @@ impl Assembler
}
},
// Instruction output whose live range spans beyond this instruction
(Opnd::InsnOut { .. }, _) => {
let idx = match original_opnds[0] {
Opnd::InsnOut { idx, .. } => {
idx
},
_ => panic!("nooooo")
};
// Our input must be from a previous instruction!
assert!(idx < index);
(Opnd::InsnOut { idx, .. }, _) => {
if live_ranges[idx] > index {
(asm.load(opnds[0]), opnds[1])
} else {
@ -162,24 +174,14 @@ impl Assembler
_ => (opnds[0], opnds[1])
};
asm.push_insn(op, vec![opnd0, opnd1], target, text, pos_marker);
asm.push_insn(insn.op, vec![opnd0, opnd1], insn.target, insn.text, insn.pos_marker);
},
// These instructions modify their input operand in-place, so we
// may need to load the input value to preserve it
Op::LShift | Op::RShift | Op::URShift => {
let (opnd0, opnd1) = match (opnds[0], opnds[1]) {
let (opnd0, opnd1) = match (insn.opnds[0], insn.opnds[1]) {
// Instruction output whose live range spans beyond this instruction
(Opnd::InsnOut { .. }, _) => {
let idx = match original_opnds[0] {
Opnd::InsnOut { idx, .. } => {
idx
},
_ => unreachable!()
};
// Our input must be from a previous instruction!
assert!(idx < index);
(Opnd::InsnOut { idx, .. }, _) => {
if live_ranges[idx] > index {
(asm.load(opnds[0]), opnds[1])
} else {
@ -193,7 +195,7 @@ impl Assembler
_ => (opnds[0], opnds[1])
};
asm.push_insn(op, vec![opnd0, opnd1], target, text, pos_marker);
asm.push_insn(insn.op, vec![opnd0, opnd1], insn.target, insn.text, insn.pos_marker);
},
Op::CSelZ | Op::CSelNZ | Op::CSelE | Op::CSelNE |
Op::CSelL | Op::CSelLE | Op::CSelG | Op::CSelGE => {
@ -204,7 +206,7 @@ impl Assembler
}
}).collect();
asm.push_insn(op, new_opnds, target, text, pos_marker);
asm.push_insn(insn.op, new_opnds, insn.target, insn.text, insn.pos_marker);
},
Op::Mov => {
match (opnds[0], opnds[1]) {
@ -236,7 +238,7 @@ impl Assembler
}
},
Op::Not => {
let opnd0 = match opnds[0] {
let opnd0 = match insn.opnds[0] {
// If we have an instruction output whose live range
// spans beyond this instruction, we have to load it.
Opnd::InsnOut { idx, .. } => {
@ -248,7 +250,9 @@ impl Assembler
},
// We have to load memory and register operands to avoid
// corrupting them.
Opnd::Mem(_) | Opnd::Reg(_) => asm.load(opnds[0]),
Opnd::Mem(_) | Opnd::Reg(_) => {
asm.load(opnds[0])
},
// Otherwise we can just reuse the existing operand.
_ => opnds[0]
};
@ -256,10 +260,14 @@ impl Assembler
asm.not(opnd0);
},
_ => {
asm.push_insn(op, opnds, target, text, pos_marker);
asm.push_insn(insn.op, opnds, insn.target, insn.text, insn.pos_marker);
}
};
})
iterator.map_insn_index(&mut asm);
}
asm
}
/// Emit platform-specific machine code