use std::cmp::Ordering;
use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet, VecDeque};
use std::fmt;
pub type PhysReg = u32;
pub type VirtReg = u32;
pub type Cycle = u64;
pub type NodeId = usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Reg {
Virt(VirtReg),
Phys(PhysReg),
}
impl Reg {
pub fn id(self) -> u32 {
match self {
Reg::Virt(v) => v,
Reg::Phys(p) => p,
}
}
pub fn is_phys(self) -> bool {
matches!(self, Reg::Phys(_))
}
pub fn is_virt(self) -> bool {
matches!(self, Reg::Virt(_))
}
}
impl fmt::Display for Reg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Reg::Virt(v) => write!(f, "v{}", v),
Reg::Phys(p) => write!(f, "p{}", p),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DepKind {
True,
Anti,
Output,
Memory,
Control,
Chain,
}
impl DepKind {
pub fn is_true(self) -> bool {
matches!(self, DepKind::True)
}
pub fn is_renamable(self) -> bool {
matches!(self, DepKind::Anti | DepKind::Output)
}
pub fn is_memory(self) -> bool {
matches!(self, DepKind::Memory)
}
pub fn as_str(self) -> &'static str {
match self {
DepKind::True => "true",
DepKind::Anti => "anti",
DepKind::Output => "output",
DepKind::Memory => "memory",
DepKind::Control => "control",
DepKind::Chain => "chain",
}
}
}
#[derive(Debug, Clone)]
pub struct DepEdge {
pub from: NodeId,
pub to: NodeId,
pub kind: DepKind,
pub reg: Option<Reg>,
pub min_latency: u32,
}
impl DepEdge {
pub fn new(from: NodeId, to: NodeId, kind: DepKind, min_latency: u32) -> Self {
Self {
from,
to,
kind,
reg: None,
min_latency,
}
}
pub fn with_reg(from: NodeId, to: NodeId, kind: DepKind, reg: Reg, min_latency: u32) -> Self {
Self {
from,
to,
kind,
reg: Some(reg),
min_latency,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SchedOperand {
Reg(Reg),
Imm(i64),
Label(String),
Global(String),
StackSlot(i32),
Mem { base: Option<Reg>, offset: i64 },
}
#[derive(Debug, Clone)]
pub struct SchedInstr {
pub index: usize,
pub opcode: u32,
pub operands: Vec<SchedOperand>,
pub defs: Vec<Reg>,
pub uses: Vec<Reg>,
pub may_load: bool,
pub may_store: bool,
pub is_branch: bool,
pub is_call: bool,
pub has_side_effects: bool,
pub latency: u32,
pub is_parallel: bool,
pub micro_ops: u32,
}
impl SchedInstr {
pub fn new(index: usize, opcode: u32) -> Self {
Self {
index,
opcode,
operands: Vec::new(),
defs: Vec::new(),
uses: Vec::new(),
may_load: false,
may_store: false,
is_branch: false,
is_call: false,
has_side_effects: false,
latency: 1,
is_parallel: false,
micro_ops: 1,
}
}
pub fn with_latency(mut self, latency: u32) -> Self {
self.latency = latency;
self
}
pub fn def(mut self, reg: Reg) -> Self {
self.defs.push(reg);
self
}
pub fn use_reg(mut self, reg: Reg) -> Self {
self.uses.push(reg);
self
}
pub fn load(mut self) -> Self {
self.may_load = true;
self
}
pub fn store(mut self) -> Self {
self.may_store = true;
self
}
pub fn branch(mut self) -> Self {
self.is_branch = true;
self
}
pub fn call(mut self) -> Self {
self.is_call = true;
self
}
pub fn side_effects(mut self) -> Self {
self.has_side_effects = true;
self
}
}
#[derive(Debug, Clone)]
pub struct PipelineStage {
pub unit: FuncUnitKind,
pub cycles: u32,
pub overlap: bool,
}
impl PipelineStage {
pub fn new(unit: FuncUnitKind, cycles: u32) -> Self {
Self {
unit,
cycles,
overlap: true,
}
}
pub fn non_overlapping(mut self) -> Self {
self.overlap = false;
self
}
}
#[derive(Debug, Clone)]
pub struct InstrItinerary {
pub opcode: u32,
pub mnemonic: String,
pub stages: Vec<PipelineStage>,
pub latency: u32,
pub micro_ops: u32,
pub dual_issue: bool,
}
impl InstrItinerary {
pub fn new(opcode: u32, mnemonic: &str) -> Self {
Self {
opcode,
mnemonic: mnemonic.to_string(),
stages: Vec::new(),
latency: 0,
micro_ops: 1,
dual_issue: false,
}
}
pub fn with_stage(mut self, unit: FuncUnitKind, cycles: u32) -> Self {
self.stages.push(PipelineStage::new(unit, cycles));
self.latency += cycles;
self
}
pub fn with_micro_ops(mut self, count: u32) -> Self {
self.micro_ops = count;
self
}
pub fn total_cycles(&self) -> u32 {
self.stages.iter().map(|s| s.cycles).sum()
}
pub fn uses_unit(&self, unit: FuncUnitKind) -> bool {
self.stages.iter().any(|s| s.unit == unit)
}
}
#[derive(Debug, Clone)]
pub struct MicroOp {
pub unit: FuncUnitKind,
pub latency: u32,
pub defs: Vec<Reg>,
pub uses: Vec<Reg>,
}
impl MicroOp {
pub fn new(unit: FuncUnitKind, latency: u32) -> Self {
Self {
unit,
latency,
defs: Vec::new(),
uses: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct MicroOpDecomposition {
pub opcode: u32,
pub uops: Vec<MicroOp>,
pub total_latency: u32,
}
impl MicroOpDecomposition {
pub fn new(opcode: u32, uops: Vec<MicroOp>) -> Self {
let total_latency = uops.iter().map(|u| u.latency).sum();
Self {
opcode,
uops,
total_latency,
}
}
pub fn count(&self) -> usize {
self.uops.len()
}
}
#[derive(Debug, Clone)]
pub struct DepNode {
pub instr: SchedInstr,
pub id: NodeId,
pub succs: Vec<DepEdge>,
pub preds: Vec<DepEdge>,
pub asap: Cycle,
pub alap: Cycle,
pub mobility: u64,
pub depth: u64,
pub height: u64,
pub unscheduled_preds: usize,
pub scheduled: bool,
pub scheduled_cycle: Cycle,
pub priority: i64,
}
impl DepNode {
pub fn new(id: NodeId, instr: SchedInstr) -> Self {
Self {
instr,
id,
succs: Vec::new(),
preds: Vec::new(),
asap: 0,
alap: 0,
mobility: 0,
depth: 0,
height: 0,
unscheduled_preds: 0,
scheduled: false,
scheduled_cycle: 0,
priority: 0,
}
}
pub fn opcode(&self) -> u32 {
self.instr.opcode
}
pub fn latency(&self) -> u32 {
self.instr.latency
}
pub fn is_sink(&self) -> bool {
self.succs.is_empty()
}
pub fn is_source(&self) -> bool {
self.preds.is_empty()
}
pub fn is_ready(&self) -> bool {
self.unscheduled_preds == 0 && !self.scheduled
}
pub fn earliest_start(&self, nodes: &[DepNode]) -> Cycle {
let mut max_cycle: Cycle = 0;
for edge in &self.preds {
let pred = &nodes[edge.from];
if pred.scheduled {
let finish = pred.scheduled_cycle + pred.latency() as u64 + edge.min_latency as u64;
if finish > max_cycle {
max_cycle = finish;
}
}
}
max_cycle
}
}
#[derive(Debug, Clone)]
pub struct DependenceGraph {
pub nodes: Vec<DepNode>,
pub num_instrs: usize,
pub entry_node: Option<NodeId>,
pub exit_node: Option<NodeId>,
pub precise_memory: bool,
pub include_anti_output: bool,
}
impl DependenceGraph {
pub fn new() -> Self {
Self {
nodes: Vec::new(),
num_instrs: 0,
entry_node: None,
exit_node: None,
precise_memory: false,
include_anti_output: true,
}
}
pub fn build(&mut self, instrs: &[SchedInstr]) {
self.nodes.clear();
self.num_instrs = instrs.len();
if instrs.is_empty() {
return;
}
for (i, instr) in instrs.iter().enumerate() {
let mut si = instr.clone();
si.index = i;
self.nodes.push(DepNode::new(i, si));
}
let entry_instr = SchedInstr::new(self.nodes.len(), 0);
self.entry_node = Some(self.nodes.len());
self.nodes.push(DepNode::new(self.nodes.len(), entry_instr));
let exit_instr = SchedInstr::new(self.nodes.len(), 0);
self.exit_node = Some(self.nodes.len());
self.nodes.push(DepNode::new(self.nodes.len(), exit_instr));
self.build_register_deps();
self.build_memory_deps();
self.build_ordering_deps();
self.connect_sentinels();
}
fn build_register_deps(&mut self) {
let mut last_def: HashMap<Reg, NodeId> = HashMap::new();
let mut last_use: HashMap<Reg, NodeId> = HashMap::new();
let n = self.num_instrs;
let mut pending_edges: Vec<DepEdge> = Vec::new();
for i in 0..n {
let uses: Vec<Reg> = self.nodes[i].instr.uses.clone();
let defs: Vec<Reg> = self.nodes[i].instr.defs.clone();
for u in &uses {
if let Some(&def_node) = last_def.get(u) {
if def_node != i {
pending_edges.push(DepEdge::with_reg(def_node, i, DepKind::True, *u, 0));
}
}
last_use.insert(*u, i);
}
if self.include_anti_output {
for d in &defs {
if let Some(&use_node) = last_use.get(d) {
if use_node != i {
pending_edges.push(DepEdge::with_reg(
use_node,
i,
DepKind::Anti,
*d,
0,
));
}
}
if let Some(&prev_def) = last_def.get(d) {
if prev_def != i {
pending_edges.push(DepEdge::with_reg(
prev_def,
i,
DepKind::Output,
*d,
0,
));
}
}
last_def.insert(*d, i);
}
}
}
for edge in pending_edges {
let from = edge.from;
let to = edge.to;
self.add_edge(from, to, edge);
}
}
fn build_memory_deps(&mut self) {
let n = self.num_instrs;
let mut last_store: Option<NodeId> = None;
let mut last_mem_access: Option<NodeId> = None;
for i in 0..n {
let may_store = self.nodes[i].instr.may_store;
let may_load = self.nodes[i].instr.may_load;
if may_store {
if let Some(prev) = last_store {
let edge = DepEdge::new(prev, i, DepKind::Memory, 0);
self.add_edge(prev, i, edge);
}
if let Some(prev) = last_mem_access {
if prev != last_store.unwrap_or(prev) {
let edge = DepEdge::new(prev, i, DepKind::Memory, 0);
self.add_edge(prev, i, edge);
}
}
last_store = Some(i);
last_mem_access = Some(i);
} else if may_load {
if let Some(prev) = last_store {
let edge = DepEdge::new(prev, i, DepKind::Memory, 0);
self.add_edge(prev, i, edge);
}
last_mem_access = Some(i);
}
}
}
fn build_ordering_deps(&mut self) {
let n = self.num_instrs;
for i in 1..n {
let is_barrier = self.nodes[i - 1].instr.is_branch
|| self.nodes[i - 1].instr.is_call
|| self.nodes[i - 1].instr.has_side_effects;
if is_barrier {
let edge = DepEdge::new(i - 1, i, DepKind::Control, 0);
self.add_edge(i - 1, i, edge);
}
}
}
fn connect_sentinels(&mut self) {
let entry = self.entry_node.unwrap();
let exit = self.exit_node.unwrap();
let n = self.num_instrs;
for i in 0..n {
let preds_empty = self.nodes[i].preds.is_empty();
let succs_empty = self.nodes[i].succs.is_empty();
if preds_empty {
let edge = DepEdge::new(entry, i, DepKind::Chain, 0);
self.add_edge(entry, i, edge);
}
if succs_empty {
let edge = DepEdge::new(i, exit, DepKind::Chain, 0);
self.add_edge(i, exit, edge);
}
}
}
fn add_edge(&mut self, from: NodeId, to: NodeId, edge: DepEdge) {
for existing in &self.nodes[from].succs {
if existing.to == to && existing.kind == edge.kind && existing.reg == edge.reg {
return;
}
}
self.nodes[from].succs.push(edge.clone());
self.nodes[to].preds.push(edge);
}
pub fn transitive_succs(&self, node: NodeId) -> HashSet<NodeId> {
let mut visited = HashSet::new();
let mut stack = vec![node];
while let Some(current) = stack.pop() {
if !visited.insert(current) {
continue;
}
for edge in &self.nodes[current].succs {
stack.push(edge.to);
}
}
visited.remove(&node);
visited
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn true_deps(&self) -> Vec<&DepEdge> {
let mut result = Vec::new();
for node in &self.nodes {
for edge in &node.succs {
if edge.kind == DepKind::True {
result.push(edge);
}
}
}
result
}
pub fn anti_deps(&self) -> Vec<&DepEdge> {
let mut result = Vec::new();
for node in &self.nodes {
for edge in &node.succs {
if edge.kind == DepKind::Anti {
result.push(edge);
}
}
}
result
}
pub fn output_deps(&self) -> Vec<&DepEdge> {
let mut result = Vec::new();
for node in &self.nodes {
for edge in &node.succs {
if edge.kind == DepKind::Output {
result.push(edge);
}
}
}
result
}
pub fn count_edges(&self, kind: DepKind) -> usize {
let mut count = 0;
for node in &self.nodes {
for edge in &node.succs {
if edge.kind == kind {
count += 1;
}
}
}
count
}
pub fn has_cycle(&self) -> bool {
let n = self.nodes.len();
let mut color: Vec<u8> = vec![0; n];
fn dfs_visit(node: usize, nodes: &[DepNode], color: &mut [u8]) -> bool {
color[node] = 1; for edge in &nodes[node].succs {
let to = edge.to;
if color[to] == 1 {
return true; }
if color[to] == 0 {
if dfs_visit(to, nodes, color) {
return true;
}
}
}
color[node] = 2; false
}
for i in 0..n {
if color[i] == 0 {
if dfs_visit(i, &self.nodes, &mut color) {
return true;
}
}
}
false
}
pub fn to_dot(&self) -> String {
let mut dot = String::from("digraph DependenceGraph {\n");
dot.push_str(" rankdir=TB;\n");
dot.push_str(" node [shape=box, style=filled, fillcolor=lightyellow];\n");
for node in &self.nodes {
let label = if node.id == self.entry_node.unwrap_or(usize::MAX) {
"ENTRY".to_string()
} else if node.id == self.exit_node.unwrap_or(usize::MAX) {
"EXIT".to_string()
} else {
format!("N{}: op{}", node.id, node.instr.opcode)
};
dot.push_str(&format!(" n{} [label=\"{}\"];\n", node.id, label));
}
for node in &self.nodes {
for edge in &node.succs {
let style = match edge.kind {
DepKind::True => "solid",
DepKind::Anti => "dashed",
DepKind::Output => "dotted",
DepKind::Memory => "bold",
DepKind::Control => "dashdot",
DepKind::Chain => "invis",
};
dot.push_str(&format!(
" n{} -> n{} [style={}, label=\"{}\"];\n",
edge.from,
edge.to,
style,
edge.kind.as_str()
));
}
}
dot.push_str("}\n");
dot
}
}
impl Default for DependenceGraph {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct DepGraphAnalysis {
pub critical_path: Vec<NodeId>,
pub critical_path_length: Cycle,
pub independent_groups: Vec<Vec<NodeId>>,
pub max_parallelism: usize,
pub true_deps: usize,
pub anti_deps: usize,
pub output_deps: usize,
pub has_cycle: bool,
}
impl DepGraphAnalysis {
pub fn analyze(dg: &DependenceGraph) -> Self {
let dag = ScheduleDAG::new(dg.clone());
let critical_path = dag.critical_path_nodes();
let groups = Self::find_independent_groups(dg);
let max_parallelism = groups.iter().map(|g| g.len()).max().unwrap_or(0);
Self {
critical_path_length: dag.critical_path_length,
critical_path,
independent_groups: groups,
max_parallelism,
true_deps: dg.count_edges(DepKind::True),
anti_deps: dg.count_edges(DepKind::Anti),
output_deps: dg.count_edges(DepKind::Output),
has_cycle: dg.has_cycle(),
}
}
fn find_independent_groups(dg: &DependenceGraph) -> Vec<Vec<NodeId>> {
let n = dg.num_instrs;
if n == 0 {
return Vec::new();
}
let mut adjacent: HashMap<NodeId, HashSet<NodeId>> = HashMap::new();
for i in 0..n {
adjacent.entry(i).or_default();
}
for node in &dg.nodes {
for edge in &node.succs {
if edge.from < n && edge.to < n {
adjacent.entry(edge.from).or_default().insert(edge.to);
adjacent.entry(edge.to).or_default().insert(edge.from);
}
}
}
let mut groups: Vec<Vec<NodeId>> = Vec::new();
for i in 0..n {
let conflicts = adjacent.get(&i).cloned().unwrap_or_default();
let mut placed = false;
for group in &mut groups {
let has_conflict = group.iter().any(|&g| conflicts.contains(&g));
if !has_conflict {
group.push(i);
placed = true;
break;
}
}
if !placed {
groups.push(vec![i]);
}
}
groups
}
pub fn height_range(dg: &DependenceGraph) -> (u64, u64) {
let mut min_h = u64::MAX;
let mut max_h = 0u64;
for node in &dg.nodes {
min_h = min_h.min(node.height);
max_h = max_h.max(node.height);
}
(min_h, max_h)
}
pub fn nodes_by_depth(dg: &DependenceGraph) -> Vec<NodeId> {
let mut indices: Vec<NodeId> = (0..dg.num_instrs).collect();
indices.sort_by_key(|&i| -(dg.nodes[i].depth as i64));
indices
}
pub fn enumerate_paths(
dg: &DependenceGraph,
source: NodeId,
sink: NodeId,
max_paths: usize,
) -> Vec<Vec<NodeId>> {
let mut paths = Vec::new();
let mut current_path = Vec::new();
let mut visited = HashSet::new();
Self::dfs_paths(
dg,
source,
sink,
&mut current_path,
&mut visited,
&mut paths,
max_paths,
);
paths
}
fn dfs_paths(
dg: &DependenceGraph,
node: NodeId,
sink: NodeId,
current: &mut Vec<NodeId>,
visited: &mut HashSet<NodeId>,
paths: &mut Vec<Vec<NodeId>>,
max_paths: usize,
) {
if paths.len() >= max_paths {
return;
}
visited.insert(node);
current.push(node);
if node == sink {
paths.push(current.clone());
} else {
for edge in &dg.nodes[node].succs {
if !visited.contains(&edge.to) {
Self::dfs_paths(dg, edge.to, sink, current, visited, paths, max_paths);
}
}
}
current.pop();
visited.remove(&node);
}
}
#[derive(Debug, Clone)]
pub struct ScheduleDAG {
graph: DependenceGraph,
pub critical_path_length: Cycle,
pub topo_order: Vec<NodeId>,
pub rev_topo_order: Vec<NodeId>,
}
impl ScheduleDAG {
pub fn new(graph: DependenceGraph) -> Self {
let mut dag = Self {
graph,
critical_path_length: 0,
topo_order: Vec::new(),
rev_topo_order: Vec::new(),
};
dag.compute_topo_order();
dag.compute_asap();
dag.compute_alap();
dag.compute_mobility();
dag.compute_depth_height();
dag
}
fn compute_topo_order(&mut self) {
let n = self.graph.nodes.len();
let mut in_degree: Vec<usize> = vec![0; n];
for node in &self.graph.nodes {
for edge in &node.succs {
in_degree[edge.to] += 1;
}
}
let mut queue: VecDeque<NodeId> = VecDeque::new();
for i in 0..n {
if in_degree[i] == 0 {
queue.push_back(i);
}
}
self.topo_order.clear();
while let Some(node) = queue.pop_front() {
self.topo_order.push(node);
for edge in &self.graph.nodes[node].succs {
in_degree[edge.to] -= 1;
if in_degree[edge.to] == 0 {
queue.push_back(edge.to);
}
}
}
self.rev_topo_order = self.topo_order.clone();
self.rev_topo_order.reverse();
}
fn compute_asap(&mut self) {
for &node_id in &self.topo_order {
let mut max_pred_finish: Cycle = 0;
for edge in &self.graph.nodes[node_id].preds {
let pred = &self.graph.nodes[edge.from];
let pred_finish = pred.asap + pred.latency() as u64;
if pred_finish > max_pred_finish {
max_pred_finish = pred_finish;
}
}
self.graph.nodes[node_id].asap = max_pred_finish;
if max_pred_finish > self.critical_path_length {
self.critical_path_length = max_pred_finish;
}
}
}
fn compute_alap(&mut self) {
let deadline = self.critical_path_length;
for node in &mut self.graph.nodes {
node.alap = deadline;
}
for &node_id in &self.rev_topo_order {
let mut min_succ_start: Cycle = deadline;
for edge in &self.graph.nodes[node_id].succs {
let succ = &self.graph.nodes[edge.to];
let succ_start = succ.alap;
if succ_start < min_succ_start {
min_succ_start = succ_start;
}
}
let my_latency = self.graph.nodes[node_id].latency() as u64;
let my_alap = if min_succ_start >= my_latency {
min_succ_start - my_latency
} else {
0
};
self.graph.nodes[node_id].alap = my_alap;
}
}
fn compute_mobility(&mut self) {
for node in &mut self.graph.nodes {
let mobility = if node.alap >= node.asap {
node.alap - node.asap
} else {
0
};
node.mobility = mobility;
}
}
fn compute_depth_height(&mut self) {
for &node_id in &self.topo_order {
let node_latency = self.graph.nodes[node_id].latency() as u64;
let mut max_height: u64 = 0;
for edge in &self.graph.nodes[node_id].preds {
let pred_height = self.graph.nodes[edge.from].height;
if pred_height > max_height {
max_height = pred_height;
}
}
self.graph.nodes[node_id].height = max_height + node_latency;
}
for &node_id in self.rev_topo_order.iter().rev() {
let node_latency = self.graph.nodes[node_id].latency() as u64;
let mut max_depth: u64 = 0;
for edge in &self.graph.nodes[node_id].succs {
let succ_depth = self.graph.nodes[edge.to].depth;
if succ_depth > max_depth {
max_depth = succ_depth;
}
}
self.graph.nodes[node_id].depth = max_depth + node_latency;
}
}
pub fn graph(&self) -> &DependenceGraph {
&self.graph
}
pub fn graph_mut(&mut self) -> &mut DependenceGraph {
&mut self.graph
}
pub fn critical_path_nodes(&self) -> Vec<NodeId> {
self.graph
.nodes
.iter()
.filter(|n| n.mobility == 0 && !n.instr.opcode == 0)
.map(|n| n.id)
.collect()
}
pub fn asap(&self, node: NodeId) -> Cycle {
self.graph.nodes[node].asap
}
pub fn alap(&self, node: NodeId) -> Cycle {
self.graph.nodes[node].alap
}
pub fn mobility(&self, node: NodeId) -> u64 {
self.graph.nodes[node].mobility
}
pub fn asap_schedule(&self) -> HashMap<NodeId, Cycle> {
let mut sched = HashMap::new();
for &node_id in &self.topo_order {
sched.insert(node_id, self.graph.nodes[node_id].asap);
}
sched
}
pub fn alap_schedule(&self) -> HashMap<NodeId, Cycle> {
let mut sched = HashMap::new();
for &node_id in &self.topo_order {
sched.insert(node_id, self.graph.nodes[node_id].alap);
}
sched
}
pub fn estimate_schedule_length(&self, _resource_model: &ResourceModel) -> Cycle {
let total_work: u64 = self.graph.nodes.iter().map(|n| n.latency() as u64).sum();
let issue_bound = total_work / _resource_model.issue_width as u64;
std::cmp::max(self.critical_path_length, issue_bound)
}
pub fn display(&self) -> String {
let mut s = String::new();
s.push_str("ScheduleDAG:\n");
s.push_str(&format!(
" Critical path length: {}\n",
self.critical_path_length
));
s.push_str(" Nodes:\n");
for &node_id in &self.topo_order {
let node = &self.graph.nodes[node_id];
if node.instr.opcode == 0
&& (Some(node_id) == self.graph.entry_node || Some(node_id) == self.graph.exit_node)
{
continue;
}
s.push_str(&format!(
" N{}: op={}, asap={}, alap={}, mobility={}, depth={}, height={}\n",
node_id,
node.instr.opcode,
node.asap,
node.alap,
node.mobility,
node.depth,
node.height,
));
}
s
}
pub fn into_graph(self) -> DependenceGraph {
self.graph
}
}
#[derive(Debug, Clone, Default)]
struct TimeSlotStats {
pub ops_count: usize,
pub resource_usage: HashMap<FuncUnitKind, f64>,
}
#[derive(Debug, Clone)]
pub struct ForceDirectedScheduler {
graph: DependenceGraph,
schedule: HashMap<NodeId, Cycle>,
mobility: HashMap<NodeId, u64>,
resource_model: ResourceModel,
distr_graph: HashMap<NodeId, Vec<f64>>,
slot_stats: BTreeMap<Cycle, TimeSlotStats>,
iterations: usize,
temperature: f64,
}
impl ForceDirectedScheduler {
pub fn new(graph: DependenceGraph, resource_model: ResourceModel) -> Self {
let dag = ScheduleDAG::new(graph.clone());
let mut mobility = HashMap::new();
let mut schedule = HashMap::new();
for node in &graph.nodes {
if node.instr.opcode != 0 {
mobility.insert(node.id, dag.mobility(node.id));
schedule.insert(node.id, dag.asap(node.id));
}
}
Self {
graph,
schedule,
mobility,
resource_model,
distr_graph: HashMap::new(),
slot_stats: BTreeMap::new(),
iterations: 0,
temperature: 1.0,
}
}
pub fn run(&mut self, max_iterations: usize) -> HashMap<NodeId, Cycle> {
for _ in 0..max_iterations {
self.build_distribution();
let changed = self.apply_forces();
self.iterations += 1;
if !changed {
break;
}
if self.temperature > 0.01 {
self.temperature *= 0.95;
}
}
self.schedule.clone()
}
fn build_distribution(&mut self) {
self.distr_graph.clear();
self.slot_stats.clear();
for node in &self.graph.nodes {
if node.instr.opcode == 0 {
continue;
}
let mobility = self.mobility.get(&node.id).copied().unwrap_or(0);
let asap = self.schedule.get(&node.id).copied().unwrap_or(0);
let window = (mobility + 1) as usize;
let prob = 1.0 / window as f64;
let distr: Vec<f64> = (0..window).map(|_| prob).collect();
self.distr_graph.insert(node.id, distr);
for t in asap..=asap + mobility {
let stats = self.slot_stats.entry(t).or_default();
stats.ops_count += 1;
}
}
}
fn apply_forces(&mut self) -> bool {
let mut changed = false;
let node_ids: Vec<NodeId> = self
.graph
.nodes
.iter()
.filter(|n| n.instr.opcode != 0)
.map(|n| n.id)
.collect();
for node_id in node_ids {
let mobility = self.mobility.get(&node_id).copied().unwrap_or(0);
let asap = self.schedule.get(&node_id).copied().unwrap_or(0);
if mobility == 0 {
continue; }
let mut best_cycle = asap;
let mut best_force = f64::MAX;
for t in asap..=asap + mobility {
let force = self.compute_force(node_id, t);
if force < best_force {
best_force = force;
best_cycle = t;
}
}
if best_cycle != asap {
self.schedule.insert(node_id, best_cycle);
changed = true;
}
}
changed
}
fn compute_force(&self, node_id: NodeId, target_cycle: Cycle) -> f64 {
let node = &self.graph.nodes[node_id];
let unit = self.resource_model.get_unit_kind(node.opcode());
let self_force = self.congestion_at(unit, target_cycle);
let mut pred_force = 0.0;
let latency = node.latency() as u64;
for edge in &node.preds {
if let Some(&pred_cycle) = self.schedule.get(&edge.from) {
let pred_latency = self.graph.nodes[edge.from].latency() as u64;
if pred_cycle + pred_latency > target_cycle {
pred_force += (pred_cycle + pred_latency - target_cycle) as f64 * 10.0;
}
}
}
let mut succ_force = 0.0;
for edge in &node.succs {
if let Some(&succ_cycle) = self.schedule.get(&edge.to) {
if target_cycle + latency > succ_cycle {
succ_force += (target_cycle + latency - succ_cycle) as f64 * 10.0;
}
}
}
self_force + pred_force + succ_force
}
fn congestion_at(&self, unit: FuncUnitKind, cycle: Cycle) -> f64 {
if let Some(stats) = self.slot_stats.get(&cycle) {
*stats.resource_usage.get(&unit).unwrap_or(&0.0)
} else {
0.0
}
}
pub fn schedule_length(&self) -> Cycle {
self.schedule.values().max().copied().unwrap_or(0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FuncUnitKind {
ALU,
Load,
Store,
Branch,
FPAdd,
FPMul,
FPDiv,
VecALU,
VecMem,
Complex,
General,
}
impl FuncUnitKind {
pub fn as_str(self) -> &'static str {
match self {
FuncUnitKind::ALU => "ALU",
FuncUnitKind::Load => "Load",
FuncUnitKind::Store => "Store",
FuncUnitKind::Branch => "Branch",
FuncUnitKind::FPAdd => "FPAdd",
FuncUnitKind::FPMul => "FPMul",
FuncUnitKind::FPDiv => "FPDiv",
FuncUnitKind::VecALU => "VecALU",
FuncUnitKind::VecMem => "VecMem",
FuncUnitKind::Complex => "Complex",
FuncUnitKind::General => "General",
}
}
}
#[derive(Debug, Clone)]
pub struct FuncUnit {
pub name: String,
pub kind: FuncUnitKind,
pub count: u32,
pub pipeline_depth: u32,
pub fully_pipelined: bool,
pub supports_forwarding: bool,
pub forwarding_cycle: u32,
}
impl FuncUnit {
pub fn new(name: &str, kind: FuncUnitKind, count: u32) -> Self {
Self {
name: name.to_string(),
kind,
count,
pipeline_depth: 1,
fully_pipelined: true,
supports_forwarding: false,
forwarding_cycle: 0,
}
}
pub fn with_pipeline(mut self, depth: u32, pipelined: bool) -> Self {
self.pipeline_depth = depth;
self.fully_pipelined = pipelined;
self
}
pub fn with_forwarding(mut self, cycle: u32) -> Self {
self.supports_forwarding = true;
self.forwarding_cycle = cycle;
self
}
}
#[derive(Debug, Clone)]
pub struct OpcodeResourceUsage {
pub opcode: u32,
pub unit: FuncUnitKind,
pub latency: u32,
pub pipelined: bool,
pub micro_ops: u32,
pub resource_cycles: Vec<(FuncUnitKind, u32)>,
}
impl OpcodeResourceUsage {
pub fn new(opcode: u32, unit: FuncUnitKind, latency: u32) -> Self {
Self {
opcode,
unit,
latency,
pipelined: true,
micro_ops: 1,
resource_cycles: vec![(unit, latency)],
}
}
}
#[derive(Debug, Clone)]
pub struct ResourceModel {
pub issue_width: u32,
pub units: Vec<FuncUnit>,
pub opcode_usage: HashMap<u32, OpcodeResourceUsage>,
pub default_latency: u32,
reservation_table: BTreeMap<Cycle, HashMap<FuncUnitKind, u32>>,
}
impl ResourceModel {
pub fn default_model() -> Self {
let mut model = Self {
issue_width: 4,
units: Vec::new(),
opcode_usage: HashMap::new(),
default_latency: 1,
reservation_table: BTreeMap::new(),
};
model
.units
.push(FuncUnit::new("ALU0", FuncUnitKind::ALU, 4));
model
.units
.push(FuncUnit::new("LoadUnit", FuncUnitKind::Load, 2).with_pipeline(3, true));
model
.units
.push(FuncUnit::new("StoreUnit", FuncUnitKind::Store, 1).with_pipeline(1, true));
model
.units
.push(FuncUnit::new("BranchUnit", FuncUnitKind::Branch, 1));
model
.units
.push(FuncUnit::new("FPAdd", FuncUnitKind::FPAdd, 1).with_pipeline(3, true));
model
.units
.push(FuncUnit::new("FPMul", FuncUnitKind::FPMul, 1).with_pipeline(5, true));
model
.units
.push(FuncUnit::new("FPDiv", FuncUnitKind::FPDiv, 1).with_pipeline(12, false));
model
.units
.push(FuncUnit::new("Complex", FuncUnitKind::Complex, 1).with_pipeline(3, false));
model
}
pub fn skylake_model() -> Self {
let mut model = Self {
issue_width: 4,
units: Vec::new(),
opcode_usage: HashMap::new(),
default_latency: 1,
reservation_table: BTreeMap::new(),
};
model
.units
.push(FuncUnit::new("Port0", FuncUnitKind::ALU, 1)); model
.units
.push(FuncUnit::new("Port1", FuncUnitKind::ALU, 1)); model
.units
.push(FuncUnit::new("Port2", FuncUnitKind::Load, 1)); model
.units
.push(FuncUnit::new("Port3", FuncUnitKind::Load, 1)); model
.units
.push(FuncUnit::new("Port4", FuncUnitKind::Store, 1)); model
.units
.push(FuncUnit::new("Port5", FuncUnitKind::ALU, 1)); model
.units
.push(FuncUnit::new("Port6", FuncUnitKind::Branch, 1)); model
.units
.push(FuncUnit::new("Port7", FuncUnitKind::Store, 1));
model
}
pub fn riscv_simple_model() -> Self {
let mut model = Self {
issue_width: 1,
units: Vec::new(),
opcode_usage: HashMap::new(),
default_latency: 1,
reservation_table: BTreeMap::new(),
};
model.units.push(FuncUnit::new("ALU", FuncUnitKind::ALU, 1));
model
.units
.push(FuncUnit::new("Mem", FuncUnitKind::Load, 1).with_pipeline(1, true));
model
.units
.push(FuncUnit::new("Branch", FuncUnitKind::Branch, 1));
model
.units
.push(FuncUnit::new("MulDiv", FuncUnitKind::Complex, 1).with_pipeline(4, false));
model
}
pub fn get_latency(&self, opcode: u32) -> u32 {
self.opcode_usage
.get(&opcode)
.map(|u| u.latency)
.unwrap_or(self.default_latency)
}
pub fn get_unit_kind(&self, opcode: u32) -> FuncUnitKind {
self.opcode_usage
.get(&opcode)
.map(|u| u.unit)
.unwrap_or(FuncUnitKind::ALU)
}
pub fn is_unit_available(&self, unit: FuncUnitKind, cycle: Cycle) -> bool {
if let Some(usage_map) = self.reservation_table.get(&cycle) {
let used = usage_map.get(&unit).copied().unwrap_or(0);
let total = self.get_unit_count(unit);
used < total
} else {
true
}
}
pub fn get_unit_count(&self, kind: FuncUnitKind) -> u32 {
self.units
.iter()
.filter(|u| u.kind == kind)
.map(|u| u.count)
.sum()
}
pub fn reserve(&mut self, opcode: u32, start_cycle: Cycle) {
let usage = self
.opcode_usage
.get(&opcode)
.cloned()
.unwrap_or_else(|| OpcodeResourceUsage::new(opcode, FuncUnitKind::ALU, 1));
let latency = usage.latency as u64;
let unit = usage.unit;
for cyc in start_cycle..start_cycle + latency {
let entry = self
.reservation_table
.entry(cyc)
.or_insert_with(HashMap::new);
*entry.entry(unit).or_insert(0) += 1;
}
}
pub fn can_issue(&self, opcodes: &[u32], cycle: Cycle) -> bool {
if opcodes.len() as u32 > self.issue_width {
return false;
}
for &opc in opcodes {
let unit = self.get_unit_kind(opc);
if !self.is_unit_available(unit, cycle) {
return false;
}
}
true
}
pub fn find_issue_slot(&self, opcode: u32, earliest: Cycle) -> Cycle {
let mut cycle = earliest;
loop {
let unit = self.get_unit_kind(opcode);
if self.is_unit_available(unit, cycle) {
return cycle;
}
cycle += 1;
}
}
pub fn clear_reservations(&mut self) {
self.reservation_table.clear();
}
pub fn max_reserved_cycle(&self) -> Option<Cycle> {
self.reservation_table.keys().last().copied()
}
pub fn utilization_summary(&self) -> String {
let mut s = String::from("ResourceModel Utilization:\n");
s.push_str(&format!(" Issue width: {}\n", self.issue_width));
s.push_str(" Functional units:\n");
for unit in &self.units {
let count = self.get_unit_count(unit.kind);
s.push_str(&format!(
" {} ({:?}): {} instance(s)\n",
unit.name, unit.kind, count
));
}
if !self.reservation_table.is_empty() {
s.push_str(" Reservation table:\n");
for (cycle, usage) in self.reservation_table.iter().take(20) {
s.push_str(&format!(" Cycle {}: ", cycle));
for (unit, count) in usage {
s.push_str(&format!("{:?}={} ", unit, count));
}
s.push('\n');
}
}
s
}
}
impl Default for ResourceModel {
fn default() -> Self {
Self::default_model()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ForwardingPath {
None,
Full,
Delayed(u32),
Restricted(FuncUnitKind),
}
#[derive(Debug, Clone)]
pub struct Hazard {
pub kind: HazardKind,
pub instr_idx: usize,
pub resource: String,
pub stall_cycles: u32,
pub can_forward: bool,
}
impl Hazard {
pub fn new(kind: HazardKind, instr_idx: usize, resource: &str, stalls: u32) -> Self {
Self {
kind,
instr_idx,
resource: resource.to_string(),
stall_cycles: stalls,
can_forward: false,
}
}
pub fn with_forwarding(mut self) -> Self {
self.can_forward = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HazardKind {
Structural,
ReadAfterWrite,
WriteAfterRead,
WriteAfterWrite,
Control,
MemoryOrder,
IssueWidth,
}
impl HazardKind {
pub fn as_str(self) -> &'static str {
match self {
HazardKind::Structural => "structural",
HazardKind::ReadAfterWrite => "RAW",
HazardKind::WriteAfterRead => "WAR",
HazardKind::WriteAfterWrite => "WAW",
HazardKind::Control => "control",
HazardKind::MemoryOrder => "memory_order",
HazardKind::IssueWidth => "issue_width",
}
}
}
#[derive(Debug, Clone)]
pub struct HazardRecognizer {
pub resource_model: ResourceModel,
forwarding_paths: HashMap<(FuncUnitKind, FuncUnitKind), ForwardingPath>,
scoreboard: HashMap<Reg, (NodeId, Cycle)>,
pub current_cycle: Cycle,
pub issued: Vec<(NodeId, Cycle)>,
pub enable_forwarding: bool,
}
impl HazardRecognizer {
pub fn new(resource_model: ResourceModel) -> Self {
Self {
resource_model,
forwarding_paths: HashMap::new(),
scoreboard: HashMap::new(),
current_cycle: 0,
issued: Vec::new(),
enable_forwarding: true,
}
}
pub fn add_forwarding(
&mut self,
producer: FuncUnitKind,
consumer: FuncUnitKind,
path: ForwardingPath,
) {
self.forwarding_paths.insert((producer, consumer), path);
}
pub fn check_structural(&self, opcode: u32, at_cycle: Cycle) -> Option<Hazard> {
let unit = self.resource_model.get_unit_kind(opcode);
if !self.resource_model.is_unit_available(unit, at_cycle) {
return Some(Hazard::new(
HazardKind::Structural,
0,
unit.as_str(),
1, ));
}
None
}
pub fn check_raw(&self, node: &DepNode, nodes: &[DepNode]) -> Vec<Hazard> {
let mut hazards = Vec::new();
for u in &node.instr.uses {
if let Some(&(prod_node, write_cycle)) = self.scoreboard.get(u) {
let producer = &nodes[prod_node];
let effective_latency = if self.enable_forwarding {
self.get_forwarding_latency(producer.opcode(), node.opcode())
} else {
producer.latency() as u64
};
let available_at = write_cycle + effective_latency;
if self.current_cycle < available_at {
let stalls = (available_at - self.current_cycle) as u32;
let mut hazard = Hazard::new(
HazardKind::ReadAfterWrite,
node.id,
&format!("{}", u),
stalls,
);
if self.enable_forwarding
&& self.get_forwarding_latency(producer.opcode(), node.opcode())
< producer.latency() as u64
{
hazard = hazard.with_forwarding();
}
hazards.push(hazard);
}
}
}
hazards
}
pub fn check_war(&self, node: &DepNode, _nodes: &[DepNode]) -> Vec<Hazard> {
let mut hazards = Vec::new();
for d in &node.instr.defs {
if self.scoreboard.contains_key(d) {
hazards.push(Hazard::new(
HazardKind::WriteAfterRead,
node.id,
&format!("{}", d),
0, ));
}
}
hazards
}
pub fn check_waw(&self, node: &DepNode) -> Vec<Hazard> {
let mut hazards = Vec::new();
for d in &node.instr.defs {
if self.scoreboard.contains_key(d) {
hazards.push(Hazard::new(
HazardKind::WriteAfterWrite,
node.id,
&format!("{}", d),
0,
));
}
}
hazards
}
fn get_forwarding_latency(&self, producer_opcode: u32, consumer_opcode: u32) -> u64 {
let prod_unit = self.resource_model.get_unit_kind(producer_opcode);
let cons_unit = self.resource_model.get_unit_kind(consumer_opcode);
if let Some(path) = self.forwarding_paths.get(&(prod_unit, cons_unit)) {
match path {
ForwardingPath::None => self.resource_model.get_latency(producer_opcode) as u64,
ForwardingPath::Full => 0,
ForwardingPath::Delayed(d) => *d as u64,
ForwardingPath::Restricted(_) => {
(self.resource_model.get_latency(producer_opcode) / 2) as u64
}
}
} else {
self.resource_model.get_latency(producer_opcode) as u64
}
}
pub fn can_issue_at(&self, node: &DepNode, nodes: &[DepNode], at_cycle: Cycle) -> Vec<Hazard> {
let mut hazards = Vec::new();
if let Some(h) = self.check_structural(node.opcode(), at_cycle) {
hazards.push(h);
}
let issued_this_cycle = self
.issued
.iter()
.filter(|(_, cyc)| *cyc == at_cycle)
.count();
if issued_this_cycle as u32 >= self.resource_model.issue_width {
hazards.push(Hazard::new(
HazardKind::IssueWidth,
node.id,
"issue_width",
self.resource_model.issue_width,
));
}
hazards.extend(self.check_raw(node, nodes));
hazards.extend(self.check_war(node, nodes));
hazards.extend(self.check_waw(node));
hazards
}
pub fn issue(&mut self, node: &DepNode, at_cycle: Cycle) {
for d in &node.instr.defs {
self.scoreboard
.insert(*d, (node.id, at_cycle + node.latency() as u64));
}
self.issued.push((node.id, at_cycle));
self.resource_model.reserve(node.opcode(), at_cycle);
}
pub fn advance_to(&mut self, cycle: Cycle) {
self.current_cycle = cycle;
}
pub fn find_issue_cycle(&self, node: &DepNode, nodes: &[DepNode], earliest: Cycle) -> Cycle {
let mut cycle = earliest;
loop {
let hazards = self.can_issue_at(node, nodes, cycle);
if hazards.is_empty() {
return cycle;
}
let max_stall = hazards.iter().map(|h| h.stall_cycles).max().unwrap_or(1);
cycle += std::cmp::max(1, max_stall as u64);
}
}
pub fn cycle(&self) -> Cycle {
self.current_cycle
}
pub fn clear(&mut self) {
self.scoreboard.clear();
self.issued.clear();
self.current_cycle = 0;
self.resource_model.clear_reservations();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchedPriority {
CriticalPath,
Height,
Latency,
SuccessorCount,
Balanced,
ResourceAware,
RegPressure,
}
#[derive(Debug, Clone, Eq)]
struct ReadyEntry {
node_id: NodeId,
priority: i64,
tiebreaker: usize,
}
impl PartialEq for ReadyEntry {
fn eq(&self, other: &Self) -> bool {
self.priority == other.priority && self.tiebreaker == other.tiebreaker
}
}
impl PartialOrd for ReadyEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ReadyEntry {
fn cmp(&self, other: &Self) -> Ordering {
self.priority
.cmp(&other.priority)
.then_with(|| other.tiebreaker.cmp(&self.tiebreaker))
}
}
#[derive(Debug, Clone)]
pub struct ListScheduler {
pub graph: DependenceGraph,
pub topo_order: Vec<NodeId>,
ready_list: BinaryHeap<ReadyEntry>,
pub schedule: Vec<(NodeId, Cycle)>,
pub current_cycle: Cycle,
pub priority_heuristic: SchedPriority,
pub resource_model: ResourceModel,
pub hazard_recognizer: HazardRecognizer,
pub resource_aware: bool,
pub max_cycle: Cycle,
}
impl ListScheduler {
pub fn new(graph: DependenceGraph, resource_model: ResourceModel) -> Self {
let topo_order = ScheduleDAG::new(graph.clone()).topo_order;
let hazard_recognizer = HazardRecognizer::new(resource_model.clone());
Self {
graph,
topo_order,
ready_list: BinaryHeap::new(),
schedule: Vec::new(),
current_cycle: 0,
priority_heuristic: SchedPriority::CriticalPath,
resource_model,
hazard_recognizer,
resource_aware: true,
max_cycle: 0,
}
}
pub fn compute_priorities(&mut self) {
match self.priority_heuristic {
SchedPriority::CriticalPath => self.compute_critical_path_priority(),
SchedPriority::Height => self.compute_height_priority(),
SchedPriority::Latency => self.compute_latency_priority(),
SchedPriority::SuccessorCount => self.compute_successor_count_priority(),
SchedPriority::Balanced => self.compute_balanced_priority(),
SchedPriority::ResourceAware => self.compute_resource_aware_priority(),
SchedPriority::RegPressure => self.compute_reg_pressure_priority(),
}
}
fn compute_critical_path_priority(&mut self) {
let dag = ScheduleDAG::new(self.graph.clone());
for node in &mut self.graph.nodes {
node.priority = dag.graph().nodes[node.id].depth as i64;
}
}
fn compute_height_priority(&mut self) {
let dag = ScheduleDAG::new(self.graph.clone());
for node in &mut self.graph.nodes {
node.priority = dag.graph().nodes[node.id].height as i64;
}
}
fn compute_latency_priority(&mut self) {
for node in &mut self.graph.nodes {
node.priority = node.latency() as i64;
}
}
fn compute_successor_count_priority(&mut self) {
for node in &mut self.graph.nodes {
node.priority = node.succs.len() as i64;
}
}
fn compute_balanced_priority(&mut self) {
let dag = ScheduleDAG::new(self.graph.clone());
for node in &mut self.graph.nodes {
let depth = dag.graph().nodes[node.id].depth;
let mobility = dag.graph().nodes[node.id].mobility;
let mobility_penalty = if mobility == 0 { 100 } else { 0 };
node.priority = (depth * 2) as i64 + mobility_penalty;
}
}
fn compute_resource_aware_priority(&mut self) {
let dag = ScheduleDAG::new(self.graph.clone());
let mut unit_counts: HashMap<FuncUnitKind, usize> = HashMap::new();
for node in &self.graph.nodes {
let unit = self.resource_model.get_unit_kind(node.opcode());
*unit_counts.entry(unit).or_insert(0) += 1;
}
for node in &mut self.graph.nodes {
let depth = dag.graph().nodes[node.id].depth;
let unit = self.resource_model.get_unit_kind(node.opcode());
let unit_count = unit_counts.get(&unit).copied().unwrap_or(1) as i64;
let unit_available = self.resource_model.get_unit_count(unit) as i64;
let scarcity = if unit_available > 0 {
(unit_count * 10) / unit_available
} else {
10
};
node.priority = (depth as i64) * 10 + scarcity;
}
}
fn compute_reg_pressure_priority(&mut self) {
let dag = ScheduleDAG::new(self.graph.clone());
let mut use_counts: HashMap<Reg, usize> = HashMap::new();
for node in &self.graph.nodes {
for u in &node.instr.uses {
*use_counts.entry(*u).or_insert(0) += 1;
}
}
for node in &mut self.graph.nodes {
let depth = dag.graph().nodes[node.id].depth as i64;
let pressure: i64 = node
.instr
.defs
.iter()
.map(|d| *use_counts.get(d).unwrap_or(&0) as i64)
.sum();
node.priority = depth * 100 + pressure;
}
}
fn init_ready_list(&mut self) {
self.ready_list.clear();
for node in &mut self.graph.nodes {
node.unscheduled_preds = node.preds.len();
node.scheduled = false;
node.scheduled_cycle = 0;
}
for node in &self.graph.nodes {
if node.unscheduled_preds == 0 && !node.scheduled && node.instr.opcode != 0 {
self.ready_list.push(ReadyEntry {
node_id: node.id,
priority: node.priority,
tiebreaker: node.instr.index,
});
}
}
}
pub fn run(&mut self) -> Vec<(NodeId, Cycle)> {
self.compute_priorities();
self.init_ready_list();
self.schedule.clear();
self.current_cycle = 0;
self.max_cycle = 0;
while !self.ready_list.is_empty() {
let mut issued_this_cycle = false;
while !self.ready_list.is_empty() {
let ready = self.ready_list.peek().unwrap().node_id;
let node = &self.graph.nodes[ready];
if self.resource_aware {
let hazards = self.hazard_recognizer.can_issue_at(
node,
&self.graph.nodes,
self.current_cycle,
);
if !hazards.is_empty() {
let mut found = false;
let mut deferred = Vec::new();
while let Some(entry) = self.ready_list.pop() {
let n = &self.graph.nodes[entry.node_id];
let hz = self.hazard_recognizer.can_issue_at(
n,
&self.graph.nodes,
self.current_cycle,
);
if hz.is_empty() {
self.issue_node(entry.node_id);
found = true;
issued_this_cycle = true;
break;
} else {
deferred.push(entry);
}
}
for entry in deferred {
self.ready_list.push(entry);
}
if !found {
break; }
} else {
self.ready_list.pop();
self.issue_node(ready);
issued_this_cycle = true;
}
} else {
self.ready_list.pop();
self.issue_node(ready);
issued_this_cycle = true;
}
}
if !issued_this_cycle {
self.current_cycle += 1;
self.update_ready_list();
}
}
self.max_cycle = self.current_cycle;
self.schedule.clone()
}
fn issue_node(&mut self, node_id: NodeId) {
let node_clone = self.graph.nodes[node_id].clone();
self.graph.nodes[node_id].scheduled = true;
self.graph.nodes[node_id].scheduled_cycle = self.current_cycle;
self.schedule.push((node_id, self.current_cycle));
self.hazard_recognizer
.issue(&node_clone, self.current_cycle);
let succs: Vec<NodeId> = self.graph.nodes[node_id]
.succs
.iter()
.map(|e| e.to)
.collect();
for succ in succs {
self.graph.nodes[succ].unscheduled_preds =
self.graph.nodes[succ].unscheduled_preds.saturating_sub(1);
}
self.update_ready_list();
}
fn update_ready_list(&mut self) {
for node in &mut self.graph.nodes {
if node.unscheduled_preds == 0 && !node.scheduled && node.instr.opcode != 0 {
self.ready_list.push(ReadyEntry {
node_id: node.id,
priority: node.priority,
tiebreaker: node.instr.index,
});
node.unscheduled_preds = usize::MAX; }
}
}
pub fn schedule_length(&self) -> Cycle {
self.max_cycle
}
pub fn reorder_instructions(&self, original: &[SchedInstr]) -> Vec<SchedInstr> {
let mut reordered: Vec<SchedInstr> = Vec::with_capacity(original.len());
let mut sched_map: HashMap<NodeId, Cycle> = HashMap::new();
for &(node_id, cycle) in &self.schedule {
sched_map.insert(node_id, cycle);
}
let mut sorted: Vec<(NodeId, Cycle)> = self.schedule.clone();
sorted.sort_by_key(|&(n, c)| (c, n));
for (node_id, _cycle) in sorted {
if node_id < original.len() {
reordered.push(original[node_id].clone());
}
}
reordered
}
pub fn with_priority(mut self, priority: SchedPriority) -> Self {
self.priority_heuristic = priority;
self
}
pub fn with_resource_aware(mut self, aware: bool) -> Self {
self.resource_aware = aware;
self
}
pub fn display_schedule(&self) -> String {
let mut s = String::new();
s.push_str("ListScheduler Schedule:\n");
s.push_str(&format!(
" Total nodes scheduled: {}\n",
self.schedule.len()
));
s.push_str(&format!(" Schedule length: {} cycles\n", self.max_cycle));
s.push_str(" Cycle → Instructions:\n");
let mut by_cycle: BTreeMap<Cycle, Vec<NodeId>> = BTreeMap::new();
for &(node_id, cycle) in &self.schedule {
by_cycle.entry(cycle).or_default().push(node_id);
}
for (cycle, nodes) in by_cycle {
s.push_str(&format!(" Cycle {}: ", cycle));
for n in nodes {
s.push_str(&format!("N{}(op={}) ", n, self.graph.nodes[n].opcode()));
}
s.push('\n');
}
s
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchedDirection {
TopDown,
BottomUp,
Bidirectional,
}
#[derive(Debug, Clone)]
pub struct BidirectionalScheduler {
top_down: ListScheduler,
bottom_up: Option<ListScheduler>,
pub direction: SchedDirection,
pub td_schedule: Vec<(NodeId, Cycle)>,
pub bu_schedule: Vec<(NodeId, Cycle)>,
pub best_schedule: Vec<(NodeId, Cycle)>,
pub best_length: Cycle,
}
impl BidirectionalScheduler {
pub fn new(graph: DependenceGraph, resource_model: ResourceModel) -> Self {
let mut rev_graph = graph.clone();
for node in &mut rev_graph.nodes {
std::mem::swap(&mut node.succs, &mut node.preds);
}
let td = ListScheduler::new(graph, resource_model.clone());
let bu = ListScheduler::new(rev_graph, resource_model);
Self {
top_down: td,
bottom_up: Some(bu),
direction: SchedDirection::Bidirectional,
td_schedule: Vec::new(),
bu_schedule: Vec::new(),
best_schedule: Vec::new(),
best_length: 0,
}
}
pub fn run(&mut self) -> Vec<(NodeId, Cycle)> {
match self.direction {
SchedDirection::TopDown => {
self.td_schedule = self.top_down.run();
self.best_schedule = self.td_schedule.clone();
self.best_length = self.top_down.schedule_length();
}
SchedDirection::BottomUp => {
if let Some(ref mut bu) = self.bottom_up {
self.bu_schedule = bu.run();
self.best_schedule = self.bu_schedule.clone();
self.best_length = bu.schedule_length();
}
}
SchedDirection::Bidirectional => {
self.td_schedule = self.top_down.run();
let td_len = self.top_down.schedule_length();
if let Some(ref mut bu) = self.bottom_up {
self.bu_schedule = bu.run();
let bu_len = bu.schedule_length();
if td_len <= bu_len {
self.best_schedule = self.td_schedule.clone();
self.best_length = td_len;
} else {
self.best_schedule = self.bu_schedule.clone();
self.best_length = bu_len;
}
}
}
}
self.best_schedule.clone()
}
pub fn comparison(&self) -> (Cycle, Cycle) {
(
self.top_down.schedule_length(),
self.bottom_up
.as_ref()
.map(|bu| bu.schedule_length())
.unwrap_or(0),
)
}
}
#[derive(Debug, Clone)]
pub struct PostRAReg {
pub reg: PhysReg,
pub is_live: bool,
pub last_def: Option<NodeId>,
pub pending_uses: Vec<NodeId>,
}
impl PostRAReg {
pub fn new(reg: PhysReg) -> Self {
Self {
reg,
is_live: false,
last_def: None,
pending_uses: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct PostRAScheduler {
pub schedule: Vec<(NodeId, Cycle)>,
pub reg_state: HashMap<PhysReg, PostRAReg>,
pub scratch_regs: Vec<PhysReg>,
pub break_anti_deps: bool,
pub break_output_deps: bool,
pub max_copies: usize,
pub copies_inserted: usize,
pub anti_deps_broken: usize,
pub graph: Option<DependenceGraph>,
pub hazard: Option<HazardRecognizer>,
}
impl PostRAScheduler {
pub fn new() -> Self {
Self {
schedule: Vec::new(),
reg_state: HashMap::new(),
scratch_regs: Vec::new(),
break_anti_deps: true,
break_output_deps: false,
max_copies: 16,
copies_inserted: 0,
anti_deps_broken: 0,
graph: None,
hazard: None,
}
}
pub fn with_scratch_regs(mut self, regs: &[PhysReg]) -> Self {
self.scratch_regs = regs.to_vec();
self
}
pub fn build_reg_state(&mut self, instrs: &[SchedInstr]) {
self.reg_state.clear();
for (i, instr) in instrs.iter().enumerate() {
for d in &instr.defs {
if let Reg::Phys(p) = d {
let entry = self
.reg_state
.entry(*p)
.or_insert_with(|| PostRAReg::new(*p));
entry.is_live = true;
entry.last_def = Some(i);
entry.pending_uses.clear();
}
}
for u in &instr.uses {
if let Reg::Phys(p) = u {
let entry = self
.reg_state
.entry(*p)
.or_insert_with(|| PostRAReg::new(*p));
entry.pending_uses.push(i);
}
}
}
}
pub fn has_anti_dep(&self, a: &DepNode, b: &DepNode) -> Option<PhysReg> {
for d in &b.instr.defs {
if let Reg::Phys(p) = d {
if a.instr.uses.contains(&Reg::Phys(*p)) {
return Some(*p);
}
}
}
None
}
pub fn has_output_dep(&self, a: &DepNode, b: &DepNode) -> Option<PhysReg> {
for d_a in &a.instr.defs {
if let Reg::Phys(p_a) = d_a {
for d_b in &b.instr.defs {
if let Reg::Phys(p_b) = d_b {
if p_a == p_b {
return Some(*p_a);
}
}
}
}
}
None
}
pub fn break_anti_dep(&mut self, a_idx: NodeId, b_idx: NodeId, nodes: &mut [DepNode]) -> bool {
if !self.break_anti_deps || self.copies_inserted >= self.max_copies {
return false;
}
let conflict_reg = match self.has_anti_dep(&nodes[a_idx], &nodes[b_idx]) {
Some(r) => r,
None => return true, };
let scratch = match self.scratch_regs.first().copied() {
Some(r) => r,
None => return false,
};
for u in &mut nodes[a_idx].instr.uses {
if *u == Reg::Phys(conflict_reg) {
*u = Reg::Phys(scratch);
}
}
self.copies_inserted += 1;
self.anti_deps_broken += 1;
true
}
pub fn run(&mut self, instrs: &[SchedInstr]) -> Vec<SchedInstr> {
if instrs.is_empty() {
return Vec::new();
}
self.build_reg_state(instrs);
let mut dep_graph = DependenceGraph::new();
dep_graph.include_anti_output = true;
dep_graph.build(instrs);
let mut nodes = dep_graph.nodes.clone();
if self.break_anti_deps {
let n = instrs.len();
for i in 0..n {
for j in i + 1..n {
if self.break_anti_dep(i, j, &mut nodes) {
}
}
}
}
let mut dep_graph2 = DependenceGraph::new();
let new_instrs: Vec<SchedInstr> = nodes[..nodes.len()]
.iter()
.map(|n| n.instr.clone())
.collect();
dep_graph2.include_anti_output = true;
dep_graph2.build(&new_instrs);
let resource_model = ResourceModel::default_model();
let mut scheduler = ListScheduler::new(dep_graph2.clone(), resource_model);
let schedule = scheduler.run();
let reordered = scheduler.reorder_instructions(instrs);
self.graph = Some(dep_graph2);
self.schedule = schedule;
reordered
}
pub fn stats(&self) -> PostRASchedStats {
PostRASchedStats {
copies_inserted: self.copies_inserted,
anti_deps_broken: self.anti_deps_broken,
nodes_scheduled: self.schedule.len(),
}
}
}
impl Default for PostRAScheduler {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
pub struct PostRASchedStats {
pub copies_inserted: usize,
pub anti_deps_broken: usize,
pub nodes_scheduled: usize,
}
impl fmt::Display for PostRASchedStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"PostRASched: {} nodes, {} anti-deps broken, {} copies inserted",
self.nodes_scheduled, self.anti_deps_broken, self.copies_inserted,
)
}
}
#[derive(Debug, Clone)]
pub struct InstrLiveness {
pub live_in: HashSet<PhysReg>,
pub live_out: HashSet<PhysReg>,
pub defs: HashSet<PhysReg>,
pub uses: HashSet<PhysReg>,
}
impl InstrLiveness {
pub fn new() -> Self {
Self {
live_in: HashSet::new(),
live_out: HashSet::new(),
defs: HashSet::new(),
uses: HashSet::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct RegLivenessAnalysis {
pub instr_liveness: Vec<InstrLiveness>,
pub block_live_out: HashSet<PhysReg>,
pub block_live_in: HashSet<PhysReg>,
}
impl RegLivenessAnalysis {
pub fn compute(instrs: &[SchedInstr], block_live_out: &HashSet<PhysReg>) -> Self {
let n = instrs.len();
let mut liveness: Vec<InstrLiveness> = Vec::with_capacity(n);
let mut def_sets: Vec<HashSet<PhysReg>> = Vec::with_capacity(n);
let mut use_sets: Vec<HashSet<PhysReg>> = Vec::with_capacity(n);
for instr in instrs {
let mut defs = HashSet::new();
let mut uses = HashSet::new();
for d in &instr.defs {
if let Reg::Phys(p) = d {
defs.insert(*p);
}
}
for u in &instr.uses {
if let Reg::Phys(p) = u {
uses.insert(*p);
}
}
def_sets.push(defs);
use_sets.push(uses);
liveness.push(InstrLiveness::new());
}
let mut live_out = block_live_out.clone();
for i in (0..n).rev() {
liveness[i].defs = def_sets[i].clone();
liveness[i].uses = use_sets[i].clone();
liveness[i].live_out = live_out.clone();
let mut live_in = live_out.clone();
for d in &def_sets[i] {
live_in.remove(d);
}
for u in &use_sets[i] {
live_in.insert(*u);
}
liveness[i].live_in = live_in.clone();
live_out = live_in;
}
Self {
instr_liveness: liveness,
block_live_out: block_live_out.clone(),
block_live_in: live_out,
}
}
pub fn is_dead_after(&self, instr_idx: usize, reg: PhysReg) -> bool {
if instr_idx + 1 < self.instr_liveness.len() {
!self.instr_liveness[instr_idx + 1].live_in.contains(®)
} else {
!self.block_live_out.contains(®)
}
}
pub fn find_free_reg(&self, instr_idx: usize, candidates: &[PhysReg]) -> Option<PhysReg> {
let live = &self.instr_liveness[instr_idx].live_in;
candidates.iter().find(|r| !live.contains(r)).copied()
}
pub fn pressure_profile(&self) -> Vec<usize> {
self.instr_liveness
.iter()
.map(|l| l.live_in.len())
.collect()
}
pub fn max_pressure(&self) -> usize {
self.pressure_profile().into_iter().max().unwrap_or(0)
}
pub fn avg_pressure(&self) -> f64 {
let profile = self.pressure_profile();
if profile.is_empty() {
return 0.0;
}
profile.iter().sum::<usize>() as f64 / profile.len() as f64
}
}
#[derive(Debug, Clone)]
pub struct FusionCandidate {
pub first: usize,
pub second: usize,
pub pattern: FusionPattern,
pub eligible: bool,
pub reason: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FusionPattern {
CmpJcc,
TestJcc,
DecJnz,
IncJnz,
AddJcc,
SubJcc,
AndJcc,
OrJcc,
XorJcc,
}
impl FusionPattern {
pub fn as_str(self) -> &'static str {
match self {
FusionPattern::CmpJcc => "CMP+JCC",
FusionPattern::TestJcc => "TEST+JCC",
FusionPattern::DecJnz => "DEC+JNZ",
FusionPattern::IncJnz => "INC+JNZ",
FusionPattern::AddJcc => "ADD+JCC",
FusionPattern::SubJcc => "SUB+JCC",
FusionPattern::AndJcc => "AND+JCC",
FusionPattern::OrJcc => "OR+JCC",
FusionPattern::XorJcc => "XOR+JCC",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OpCategory {
Cmp,
Test,
Dec,
Inc,
Add,
Sub,
And,
Or,
Xor,
Jcc,
Jmp,
Other,
}
#[derive(Debug, Clone)]
pub struct MacroFusion {
pub patterns: Vec<FusionPattern>,
cmp_opcodes: HashSet<u32>,
test_opcodes: HashSet<u32>,
dec_opcodes: HashSet<u32>,
inc_opcodes: HashSet<u32>,
add_opcodes: HashSet<u32>,
sub_opcodes: HashSet<u32>,
and_opcodes: HashSet<u32>,
or_opcodes: HashSet<u32>,
xor_opcodes: HashSet<u32>,
jcc_opcodes: HashSet<u32>,
pub enabled: bool,
pub fused_count: usize,
}
impl MacroFusion {
pub fn new() -> Self {
let mut mf = Self {
patterns: vec![
FusionPattern::CmpJcc,
FusionPattern::TestJcc,
FusionPattern::DecJnz,
FusionPattern::IncJnz,
FusionPattern::AddJcc,
FusionPattern::SubJcc,
FusionPattern::AndJcc,
FusionPattern::OrJcc,
],
cmp_opcodes: HashSet::new(),
test_opcodes: HashSet::new(),
dec_opcodes: HashSet::new(),
inc_opcodes: HashSet::new(),
add_opcodes: HashSet::new(),
sub_opcodes: HashSet::new(),
and_opcodes: HashSet::new(),
or_opcodes: HashSet::new(),
xor_opcodes: HashSet::new(),
jcc_opcodes: HashSet::new(),
enabled: true,
fused_count: 0,
};
mf.cmp_opcodes.insert(100); mf.cmp_opcodes.insert(101); mf.test_opcodes.insert(110); mf.test_opcodes.insert(111); mf.dec_opcodes.insert(120); mf.inc_opcodes.insert(121); mf.add_opcodes.insert(130); mf.sub_opcodes.insert(131); mf.and_opcodes.insert(132); mf.or_opcodes.insert(133); mf.xor_opcodes.insert(134);
for i in 200..216 {
mf.jcc_opcodes.insert(i);
}
mf
}
fn categorize(&self, opcode: u32) -> OpCategory {
if self.cmp_opcodes.contains(&opcode) {
OpCategory::Cmp
} else if self.test_opcodes.contains(&opcode) {
OpCategory::Test
} else if self.dec_opcodes.contains(&opcode) {
OpCategory::Dec
} else if self.inc_opcodes.contains(&opcode) {
OpCategory::Inc
} else if self.add_opcodes.contains(&opcode) {
OpCategory::Add
} else if self.sub_opcodes.contains(&opcode) {
OpCategory::Sub
} else if self.and_opcodes.contains(&opcode) {
OpCategory::And
} else if self.or_opcodes.contains(&opcode) {
OpCategory::Or
} else if self.xor_opcodes.contains(&opcode) {
OpCategory::Xor
} else if self.jcc_opcodes.contains(&opcode) {
OpCategory::Jcc
} else {
OpCategory::Other
}
}
fn detect_pattern(&self, first_opcode: u32, second_opcode: u32) -> Option<FusionPattern> {
let cat1 = self.categorize(first_opcode);
let cat2 = self.categorize(second_opcode);
if cat2 != OpCategory::Jcc {
return None;
}
match cat1 {
OpCategory::Cmp => Some(FusionPattern::CmpJcc),
OpCategory::Test => Some(FusionPattern::TestJcc),
OpCategory::Dec => Some(FusionPattern::DecJnz),
OpCategory::Inc => Some(FusionPattern::IncJnz),
OpCategory::Add => Some(FusionPattern::AddJcc),
OpCategory::Sub => Some(FusionPattern::SubJcc),
OpCategory::And => Some(FusionPattern::AndJcc),
OpCategory::Or => Some(FusionPattern::OrJcc),
OpCategory::Xor => Some(FusionPattern::XorJcc),
_ => None,
}
}
pub fn analyze(&self, instrs: &[SchedInstr]) -> Vec<FusionCandidate> {
let mut candidates = Vec::new();
if instrs.len() < 2 {
return candidates;
}
for i in 0..instrs.len() - 1 {
let first = &instrs[i];
let second = &instrs[i + 1];
if let Some(pattern) = self.detect_pattern(first.opcode, second.opcode) {
let mut candidate = FusionCandidate {
first: i,
second: i + 1,
pattern,
eligible: true,
reason: None,
};
if !self.is_eligible(first, second, &pattern) {
candidate.eligible = false;
candidate.reason = Some(self.rejection_reason(first, second, &pattern));
}
candidates.push(candidate);
}
}
candidates
}
fn is_eligible(
&self,
first: &SchedInstr,
second: &SchedInstr,
pattern: &FusionPattern,
) -> bool {
if second
.operands
.iter()
.any(|op| matches!(op, SchedOperand::Label(_)))
{
} else {
}
if first.has_side_effects
&& !matches!(pattern, FusionPattern::CmpJcc | FusionPattern::TestJcc)
{
return false;
}
if !self.patterns.contains(pattern) {
return false;
}
true
}
fn rejection_reason(
&self,
_first: &SchedInstr,
_second: &SchedInstr,
_pattern: &FusionPattern,
) -> String {
"not eligible for fusion".to_string()
}
pub fn fuse(&mut self, instrs: &mut Vec<SchedInstr>) -> usize {
if !self.enabled {
return 0;
}
let candidates = self.analyze(instrs);
let mut fused = 0;
for candidate in candidates.iter().rev() {
if candidate.eligible {
instrs[candidate.first].is_parallel = true;
fused += 1;
}
}
self.fused_count += fused;
fused
}
pub fn supports_pattern(&self, pattern: FusionPattern) -> bool {
self.patterns.contains(&pattern)
}
pub fn add_pattern(&mut self, pattern: FusionPattern) {
if !self.patterns.contains(&pattern) {
self.patterns.push(pattern);
}
}
pub fn remove_pattern(&mut self, pattern: FusionPattern) {
self.patterns.retain(|p| *p != pattern);
}
pub fn summary(&self) -> String {
format!(
"MacroFusion: {} patterns supported, {} pairs fused",
self.patterns.len(),
self.fused_count,
)
}
}
impl Default for MacroFusion {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct FusionEligibility {
pub pattern: FusionPattern,
pub requires_flag_setting: bool,
pub requires_single_consumer: bool,
pub max_branch_displacement: Option<i64>,
pub requires_forward_branch: bool,
pub requires_same_decode_group: bool,
pub alignment: u32,
pub allows_memory_operand: bool,
pub allows_both_memory: bool,
}
impl FusionEligibility {
pub fn cmp_jcc() -> Self {
Self {
pattern: FusionPattern::CmpJcc,
requires_flag_setting: true,
requires_single_consumer: true,
max_branch_displacement: None,
requires_forward_branch: false,
requires_same_decode_group: true,
alignment: 16,
allows_memory_operand: true,
allows_both_memory: false,
}
}
pub fn test_jcc() -> Self {
Self {
pattern: FusionPattern::TestJcc,
requires_flag_setting: true,
requires_single_consumer: true,
max_branch_displacement: None,
requires_forward_branch: false,
requires_same_decode_group: true,
alignment: 16,
allows_memory_operand: true,
allows_both_memory: false,
}
}
pub fn dec_jnz() -> Self {
Self {
pattern: FusionPattern::DecJnz,
requires_flag_setting: true,
requires_single_consumer: true,
max_branch_displacement: Some(128),
requires_forward_branch: false,
requires_same_decode_group: true,
alignment: 16,
allows_memory_operand: false,
allows_both_memory: false,
}
}
pub fn check(&self, first: &SchedInstr, second: &SchedInstr) -> Result<(), String> {
if self.requires_flag_setting && first.defs.is_empty() {
return Err("First instruction must set flags".to_string());
}
if self.requires_same_decode_group {
}
if !self.allows_memory_operand && (first.may_load || first.may_store) {
return Err("Memory operand not allowed for first instruction".to_string());
}
if !self.allows_both_memory && first.may_load && second.may_load {
return Err("Both memory operands not allowed".to_string());
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct MicroOpFusion {
pub enabled: bool,
pub fusion_table: HashMap<(FuncUnitKind, FuncUnitKind), FuncUnitKind>,
pub fused_count: usize,
}
impl MicroOpFusion {
pub fn new() -> Self {
let mut fusion_table = HashMap::new();
fusion_table.insert((FuncUnitKind::ALU, FuncUnitKind::Branch), FuncUnitKind::ALU);
fusion_table.insert((FuncUnitKind::Load, FuncUnitKind::ALU), FuncUnitKind::Load);
Self {
enabled: true,
fusion_table,
fused_count: 0,
}
}
pub fn try_fuse(&self, uop1: &MicroOp, uop2: &MicroOp) -> Option<MicroOp> {
if !self.enabled {
return None;
}
if let Some(fused_unit) = self.fusion_table.get(&(uop1.unit, uop2.unit)) {
let mut fused = MicroOp::new(*fused_unit, uop1.latency.max(uop2.latency));
fused
.defs
.extend(uop1.defs.iter().chain(uop2.defs.iter()).cloned());
fused
.uses
.extend(uop1.uses.iter().chain(uop2.uses.iter()).cloned());
Some(fused)
} else {
None
}
}
pub fn fuse_decomposition(&mut self, decomp: &MicroOpDecomposition) -> MicroOpDecomposition {
let mut fused_uops = Vec::new();
let uops = &decomp.uops;
let mut i = 0;
while i < uops.len() {
if i + 1 < uops.len() {
if let Some(fused) = self.try_fuse(&uops[i], &uops[i + 1]) {
fused_uops.push(fused);
self.fused_count += 1;
i += 2;
continue;
}
}
fused_uops.push(uops[i].clone());
i += 1;
}
MicroOpDecomposition::new(decomp.opcode, fused_uops)
}
}
impl Default for MicroOpFusion {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct DeepSchedConfig {
pub priority: SchedPriority,
pub resource_aware: bool,
pub hazard_aware: bool,
pub break_anti_deps: bool,
pub macro_fusion: bool,
pub post_ra: bool,
pub issue_width: u32,
pub verbose: bool,
}
impl Default for DeepSchedConfig {
fn default() -> Self {
Self {
priority: SchedPriority::CriticalPath,
resource_aware: true,
hazard_aware: true,
break_anti_deps: true,
macro_fusion: true,
post_ra: false,
issue_width: 4,
verbose: false,
}
}
}
#[derive(Debug, Clone)]
pub struct DeepScheduler {
pub config: DeepSchedConfig,
pub dep_graph: Option<DependenceGraph>,
pub sched_dag: Option<ScheduleDAG>,
pub list_scheduler: Option<ListScheduler>,
pub post_ra_scheduler: Option<PostRAScheduler>,
pub macro_fusion: Option<MacroFusion>,
pub stats: DeepSchedStats,
}
#[derive(Debug, Clone, Default)]
pub struct DeepSchedStats {
pub num_instrs: usize,
pub true_deps: usize,
pub anti_deps: usize,
pub output_deps: usize,
pub critical_path: Cycle,
pub schedule_length: Cycle,
pub fused_pairs: usize,
pub anti_deps_broken: usize,
pub success: bool,
}
impl DeepScheduler {
pub fn new(config: DeepSchedConfig) -> Self {
Self {
config,
dep_graph: None,
sched_dag: None,
list_scheduler: None,
post_ra_scheduler: None,
macro_fusion: None,
stats: DeepSchedStats::default(),
}
}
pub fn run(&mut self, instrs: &[SchedInstr]) -> Vec<SchedInstr> {
self.stats = DeepSchedStats::default();
self.stats.num_instrs = instrs.len();
if instrs.is_empty() {
self.stats.success = true;
return Vec::new();
}
let mut dep_graph = DependenceGraph::new();
dep_graph.build(instrs);
self.stats.true_deps = dep_graph.count_edges(DepKind::True);
self.stats.anti_deps = dep_graph.count_edges(DepKind::Anti);
self.stats.output_deps = dep_graph.count_edges(DepKind::Output);
if self.config.verbose {
eprintln!("[DeepSched] Built dependence graph:");
eprintln!(
" {} nodes, {} true, {} anti, {} output edges",
dep_graph.num_instrs,
self.stats.true_deps,
self.stats.anti_deps,
self.stats.output_deps,
);
}
let sched_dag = ScheduleDAG::new(dep_graph.clone());
self.stats.critical_path = sched_dag.critical_path_length;
if self.config.verbose {
eprintln!(
"[DeepSched] ScheduleDAG: critical path = {}",
self.stats.critical_path
);
}
let resource_model = if self.config.resource_aware {
let mut rm = ResourceModel::default_model();
rm.issue_width = self.config.issue_width;
rm
} else {
ResourceModel::default_model()
};
let mut list_scheduler = ListScheduler::new(dep_graph.clone(), resource_model);
list_scheduler.priority_heuristic = self.config.priority;
list_scheduler.resource_aware = self.config.resource_aware;
let _schedule = list_scheduler.run();
self.stats.schedule_length = list_scheduler.schedule_length();
let mut reordered = list_scheduler.reorder_instructions(instrs);
if self.config.verbose {
eprintln!(
"[DeepSched] List scheduling complete: {} cycles",
self.stats.schedule_length
);
}
if self.config.post_ra {
let mut post_ra = PostRAScheduler::new();
post_ra.break_anti_deps = self.config.break_anti_deps;
let scratch_regs: Vec<PhysReg> = (32..40).collect(); post_ra = post_ra.with_scratch_regs(&scratch_regs);
reordered = post_ra.run(&reordered);
let ps = post_ra.stats();
self.stats.anti_deps_broken = ps.anti_deps_broken;
self.post_ra_scheduler = Some(post_ra);
}
if self.config.macro_fusion {
let mut fusion = MacroFusion::new();
self.stats.fused_pairs = fusion.fuse(&mut reordered);
self.macro_fusion = Some(fusion);
}
self.dep_graph = Some(dep_graph);
self.sched_dag = Some(sched_dag);
self.list_scheduler = Some(list_scheduler);
self.stats.success = true;
reordered
}
pub fn run_default(instrs: &[SchedInstr]) -> Vec<SchedInstr> {
let mut sched = Self::new(DeepSchedConfig::default());
sched.run(instrs)
}
pub fn run_pre_ra(instrs: &[SchedInstr]) -> Vec<SchedInstr> {
let config = DeepSchedConfig {
post_ra: false,
break_anti_deps: false,
..DeepSchedConfig::default()
};
let mut sched = Self::new(config);
sched.run(instrs)
}
pub fn run_post_ra(instrs: &[SchedInstr]) -> Vec<SchedInstr> {
let config = DeepSchedConfig {
post_ra: true,
break_anti_deps: true,
macro_fusion: true,
..DeepSchedConfig::default()
};
let mut sched = Self::new(config);
sched.run(instrs)
}
pub fn display_summary(&self) -> String {
let mut s = String::new();
s.push_str("=== Deep Scheduling Pipeline Summary ===\n");
s.push_str(&format!(" Instructions: {}\n", self.stats.num_instrs));
s.push_str(&format!(
" Dependences: {} true, {} anti, {} output\n",
self.stats.true_deps, self.stats.anti_deps, self.stats.output_deps,
));
s.push_str(&format!(
" Critical path: {} cycles\n",
self.stats.critical_path
));
s.push_str(&format!(
" Schedule length: {} cycles\n",
self.stats.schedule_length
));
s.push_str(&format!(" Fused pairs: {}\n", self.stats.fused_pairs));
s.push_str(&format!(
" Anti-deps broken: {}\n",
self.stats.anti_deps_broken
));
s.push_str(&format!(" Success: {}\n", self.stats.success));
s
}
}
impl Default for DeepScheduler {
fn default() -> Self {
Self::new(DeepSchedConfig::default())
}
}
#[derive(Debug, Clone)]
pub struct SchedulingRegion {
pub start: usize,
pub end: usize,
pub size: usize,
pub ends_with_branch: bool,
pub contains_call: bool,
}
impl SchedulingRegion {
pub fn new(start: usize, end: usize) -> Self {
Self {
start,
end,
size: end.saturating_sub(start),
ends_with_branch: false,
contains_call: false,
}
}
pub fn partition(instrs: &[SchedInstr]) -> Vec<Self> {
if instrs.is_empty() {
return Vec::new();
}
let mut regions = Vec::new();
let mut region_start = 0usize;
for i in 0..instrs.len() {
let is_barrier = instrs[i].is_branch || instrs[i].is_call || instrs[i].has_side_effects;
if is_barrier && i > region_start {
let mut region = Self::new(region_start, i);
region.ends_with_branch = instrs[i].is_branch;
region.contains_call = instrs[i].is_call;
regions.push(region);
region_start = i;
}
if instrs[i].is_branch && i + 1 < instrs.len() {
let mut region = Self::new(region_start, i + 1);
region.ends_with_branch = true;
regions.push(region);
region_start = i + 1;
}
}
if region_start < instrs.len() {
let region = Self::new(region_start, instrs.len());
regions.push(region);
}
regions
}
pub fn extract<'a>(&self, instrs: &'a [SchedInstr]) -> &'a [SchedInstr] {
&instrs[self.start..self.end.min(instrs.len())]
}
}
#[derive(Debug, Clone)]
pub struct VerificationResult {
pub valid: bool,
pub violations: Vec<String>,
pub edges_checked: usize,
pub resources_checked: usize,
}
impl VerificationResult {
pub fn success() -> Self {
Self {
valid: true,
violations: Vec::new(),
edges_checked: 0,
resources_checked: 0,
}
}
pub fn with_violation(msg: String) -> Self {
Self {
valid: false,
violations: vec![msg],
edges_checked: 0,
resources_checked: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct ScheduleVerifier {
pub graph: DependenceGraph,
pub resource_model: ResourceModel,
pub schedule: HashMap<NodeId, Cycle>,
}
impl ScheduleVerifier {
pub fn new(
graph: DependenceGraph,
resource_model: ResourceModel,
schedule: HashMap<NodeId, Cycle>,
) -> Self {
Self {
graph,
resource_model,
schedule,
}
}
pub fn verify(&self) -> VerificationResult {
let mut result = VerificationResult::success();
self.verify_completeness(&mut result);
self.verify_dependences(&mut result);
self.verify_resources(&mut result);
result
}
fn verify_completeness(&self, result: &mut VerificationResult) {
let n = self.graph.num_instrs;
let mut scheduled_set: HashSet<NodeId> = HashSet::new();
for (&node_id, _) in &self.schedule {
if node_id >= n {
continue; }
if !scheduled_set.insert(node_id) {
result
.violations
.push(format!("Node {} scheduled more than once", node_id));
result.valid = false;
}
}
for i in 0..n {
if !scheduled_set.contains(&i) {
result.violations.push(format!("Node {} not scheduled", i));
result.valid = false;
}
}
}
fn verify_dependences(&self, result: &mut VerificationResult) {
for node in &self.graph.nodes {
for edge in &node.succs {
result.edges_checked += 1;
let from_cycle = match self.schedule.get(&edge.from) {
Some(&c) => c,
None => continue, };
let to_cycle = match self.schedule.get(&edge.to) {
Some(&c) => c,
None => continue,
};
let from_latency = self.graph.nodes[edge.from].latency() as u64;
let min_separation = from_latency + edge.min_latency as u64;
let actual_separation = if to_cycle >= from_cycle {
to_cycle - from_cycle
} else {
result.violations.push(format!(
"Edge {}→{} ({:?}): consumer at cycle {} before producer at cycle {}",
edge.from, edge.to, edge.kind, to_cycle, from_cycle
));
result.valid = false;
continue;
};
if actual_separation < min_separation && edge.kind == DepKind::True {
result.violations.push(format!(
"Edge {}→{} ({:?}): separation {} < required {} (producer latency={})",
edge.from,
edge.to,
edge.kind,
actual_separation,
min_separation,
from_latency
));
result.valid = false;
}
}
}
}
fn verify_resources(&self, result: &mut VerificationResult) {
let mut cycle_usage: BTreeMap<Cycle, HashMap<FuncUnitKind, u32>> = BTreeMap::new();
for (&node_id, &cycle) in &self.schedule {
if node_id >= self.graph.num_instrs {
continue;
}
let opcode = self.graph.nodes[node_id].opcode();
let unit = self.resource_model.get_unit_kind(opcode);
let entry = cycle_usage.entry(cycle).or_insert_with(HashMap::new);
*entry.entry(unit).or_insert(0) += 1;
}
for (cycle, usage) in &cycle_usage {
result.resources_checked += 1;
let total_issued: u32 = usage.values().sum();
if total_issued > self.resource_model.issue_width {
result.violations.push(format!(
"Cycle {}: {} instructions issued (issue width = {})",
cycle, total_issued, self.resource_model.issue_width
));
result.valid = false;
}
for (unit, count) in usage {
let available = self.resource_model.get_unit_count(*unit);
if *count > available {
result.violations.push(format!(
"Cycle {}: {} ops on unit {:?} (available: {})",
cycle, count, unit, available
));
result.valid = false;
}
}
}
}
}
fn alu_instr(idx: usize, opcode: u32, def_reg: Option<Reg>, use_regs: &[Reg]) -> SchedInstr {
let mut instr = SchedInstr::new(idx, opcode).with_latency(1);
if let Some(def) = def_reg {
instr = instr.def(def);
}
for u in use_regs {
instr = instr.use_reg(*u);
}
instr
}
fn load_instr(idx: usize, def_reg: Reg, base_reg: Reg) -> SchedInstr {
SchedInstr::new(idx, 2)
.with_latency(3)
.def(def_reg)
.use_reg(base_reg)
.load()
}
fn store_instr(idx: usize, src_reg: Reg, base_reg: Reg) -> SchedInstr {
SchedInstr::new(idx, 3)
.with_latency(1)
.use_reg(src_reg)
.use_reg(base_reg)
.store()
}
fn branch_instr(idx: usize) -> SchedInstr {
SchedInstr::new(idx, 7).with_latency(1).branch()
}
fn fp_add_instr(idx: usize, def_reg: Reg, use1: Reg, use2: Reg) -> SchedInstr {
SchedInstr::new(idx, 4)
.with_latency(5)
.def(def_reg)
.use_reg(use1)
.use_reg(use2)
}
fn fp_mul_instr(idx: usize, def_reg: Reg, use1: Reg, use2: Reg) -> SchedInstr {
SchedInstr::new(idx, 5)
.with_latency(7)
.def(def_reg)
.use_reg(use1)
.use_reg(use2)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dep_graph_empty() {
let mut dg = DependenceGraph::new();
let instrs: Vec<SchedInstr> = Vec::new();
dg.build(&instrs);
assert!(dg.nodes.is_empty());
assert_eq!(dg.num_instrs, 0);
}
#[test]
fn test_dep_graph_true_dep() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[r1]),
alu_instr(1, 1, Some(r2), &[r0]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let true_edges = dg.true_deps();
assert!(!true_edges.is_empty(), "Should have at least one true dep");
assert_eq!(true_edges[0].from, 0);
assert_eq!(true_edges[0].to, 1);
assert_eq!(true_edges[0].reg, Some(r0));
}
#[test]
fn test_dep_graph_anti_dep() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[r1]),
alu_instr(1, 1, Some(r1), &[r0]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let anti_edges = dg.anti_deps();
assert!(!anti_edges.is_empty(), "Should have anti dep on r1");
assert_eq!(anti_edges[0].kind, DepKind::Anti);
}
#[test]
fn test_dep_graph_output_dep() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[r1]),
alu_instr(1, 1, Some(r0), &[r2]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let output_edges = dg.output_deps();
assert!(!output_edges.is_empty(), "Should have output dep on r0");
assert_eq!(output_edges[0].from, 0);
assert_eq!(output_edges[0].to, 1);
}
#[test]
fn test_dep_graph_memory_dep() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let instrs = vec![store_instr(0, r1, r0), load_instr(1, r2, r0)];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let mem_edges = dg.count_edges(DepKind::Memory);
assert!(mem_edges > 0, "Should have memory-ordering edges");
}
#[test]
fn test_dep_graph_no_cycle() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
alu_instr(2, 1, Some(r2), &[r1]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
assert!(!dg.has_cycle(), "Dependence graph should be acyclic");
}
#[test]
fn test_dep_graph_no_cycle_diamond() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let r3 = Reg::Virt(3);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
alu_instr(2, 1, Some(r2), &[r0]),
alu_instr(3, 1, Some(r3), &[r1, r2]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
assert!(!dg.has_cycle(), "Diamond graph should be acyclic");
}
#[test]
fn test_dep_graph_transitive_succs() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
alu_instr(2, 1, Some(r2), &[r1]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let succs = dg.transitive_succs(0);
assert!(succs.contains(&1), "N1 should be reachable from N0");
assert!(succs.contains(&2), "N2 should be reachable from N0");
}
#[test]
fn test_sched_dag_asap_simple() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let r3 = Reg::Virt(3);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[]),
alu_instr(2, 1, Some(r2), &[r0]),
alu_instr(3, 1, Some(r3), &[r1]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let dag = ScheduleDAG::new(dg);
assert_eq!(dag.asap(0), 0);
assert_eq!(dag.asap(1), 0);
assert_eq!(dag.asap(2), 1);
assert_eq!(dag.asap(3), 1);
}
#[test]
fn test_sched_dag_alap_simple() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let r3 = Reg::Virt(3);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[]),
alu_instr(2, 1, Some(r2), &[r0]),
alu_instr(3, 1, Some(r3), &[r1]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let dag = ScheduleDAG::new(dg);
let cpl = dag.critical_path_length;
assert!(cpl >= 1, "Critical path length should be at least 1");
assert!(dag.alap(2) <= cpl);
assert!(dag.alap(3) <= cpl);
}
#[test]
fn test_sched_dag_mobility() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let r3 = Reg::Virt(3);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]), alu_instr(1, 1, Some(r1), &[]), alu_instr(2, 1, Some(r2), &[r0]), alu_instr(3, 1, Some(r3), &[r1]), ];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let dag = ScheduleDAG::new(dg);
let mobilities: Vec<u64> = (0..4).map(|i| dag.mobility(i)).collect();
assert!(
mobilities.iter().any(|&m| m > 0),
"Should have nodes with non-zero mobility"
);
}
#[test]
fn test_sched_dag_critical_path_nodes() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let r3 = Reg::Virt(3);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
alu_instr(2, 1, Some(r2), &[r1]),
alu_instr(3, 1, Some(r3), &[r2]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let dag = ScheduleDAG::new(dg);
let critical = dag.critical_path_nodes();
assert_eq!(critical.len(), 4, "All nodes on single chain are critical");
}
#[test]
fn test_sched_dag_topological_order() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
alu_instr(2, 1, Some(r2), &[r1]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let dag = ScheduleDAG::new(dg);
let pos0 = dag.topo_order.iter().position(|&n| n == 0).unwrap();
let pos1 = dag.topo_order.iter().position(|&n| n == 1).unwrap();
let pos2 = dag.topo_order.iter().position(|&n| n == 2).unwrap();
assert!(pos0 < pos1, "N0 should come before N1 in topo order");
assert!(pos1 < pos2, "N1 should come before N2 in topo order");
}
#[test]
fn test_list_scheduler_simple_chain() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
alu_instr(2, 1, Some(r2), &[r1]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let resource_model = ResourceModel::default_model();
let mut scheduler = ListScheduler::new(dg, resource_model);
let schedule = scheduler.run();
assert!(
scheduler.schedule_length() >= 2,
"Chain should take at least 2+ cycles"
);
assert_eq!(schedule.len(), 3, "All instructions should be scheduled");
}
#[test]
fn test_list_scheduler_parallel() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let resource_model = ResourceModel::default_model();
let mut scheduler = ListScheduler::new(dg, resource_model);
let schedule = scheduler.run();
assert!(
scheduler.schedule_length() <= 2,
"Parallel ops should schedule compactly"
);
assert_eq!(schedule.len(), 2);
}
#[test]
fn test_list_scheduler_load_latency() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let rbase = Reg::Virt(100);
let instrs = vec![load_instr(0, r0, rbase), alu_instr(1, 1, Some(r1), &[r0])];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let resource_model = ResourceModel::default_model();
let mut scheduler = ListScheduler::new(dg, resource_model);
let schedule = scheduler.run();
assert_eq!(schedule.len(), 2);
let load_cycle = schedule
.iter()
.find(|&&(n, _)| n == 0)
.map(|&(_, c)| c)
.unwrap();
let alu_cycle = schedule
.iter()
.find(|&&(n, _)| n == 1)
.map(|&(_, c)| c)
.unwrap();
assert!(
alu_cycle >= load_cycle + 3,
"ALU after load must wait for load latency ({} >= {})",
alu_cycle,
load_cycle + 3
);
}
#[test]
fn test_list_scheduler_reorder() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let r3 = Reg::Virt(3);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[]),
alu_instr(2, 1, Some(r2), &[r0]),
alu_instr(3, 1, Some(r3), &[r1]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let resource_model = ResourceModel::default_model();
let mut scheduler = ListScheduler::new(dg, resource_model);
scheduler.run();
let reordered = scheduler.reorder_instructions(&instrs);
assert_eq!(reordered.len(), 4, "All instructions should be present");
}
#[test]
fn test_list_scheduler_priority_critical_path() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let r3 = Reg::Virt(3);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[]),
alu_instr(2, 1, Some(r2), &[r0]),
alu_instr(3, 1, Some(r3), &[r2]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let resource_model = ResourceModel::default_model();
let mut scheduler = ListScheduler::new(dg, resource_model);
scheduler.priority_heuristic = SchedPriority::CriticalPath;
scheduler.run();
let n0_cycle = scheduler
.schedule
.iter()
.find(|&&(n, _)| n == 0)
.map(|&(_, c)| c)
.unwrap();
assert_eq!(n0_cycle, 0, "Critical path node N0 should be at cycle 0");
}
#[test]
fn test_resource_model_default() {
let rm = ResourceModel::default_model();
assert_eq!(rm.issue_width, 4);
assert!(!rm.units.is_empty(), "Should have functional units");
assert_eq!(rm.get_latency(1), 1, "Default ALU latency should be 1");
}
#[test]
fn test_resource_model_reserve() {
let mut rm = ResourceModel::default_model();
rm.reserve(1, 0);
assert!(
rm.is_unit_available(FuncUnitKind::ALU, 1),
"ALU should be free at cycle 1 (latency=1)"
);
rm.reserve(1, 0);
rm.reserve(1, 0);
rm.reserve(1, 0);
}
#[test]
fn test_resource_model_skylake() {
let rm = ResourceModel::skylake_model();
assert!(rm.issue_width == 4 || rm.issue_width > 0);
assert!(
rm.units.len() >= 6,
"Skylake should have multiple dispatch ports"
);
}
#[test]
fn test_resource_model_riscv_simple() {
let rm = ResourceModel::riscv_simple_model();
assert_eq!(rm.issue_width, 1, "Simple RISC-V is single-issue");
assert!(rm.units.len() >= 2);
}
#[test]
fn test_hazard_recognizer_raw() {
let rm = ResourceModel::default_model();
let hr = HazardRecognizer::new(rm);
let r0 = Reg::Phys(0);
let r1 = Reg::Phys(1);
let r2 = Reg::Phys(2);
let r3 = Reg::Phys(3);
let r4 = Reg::Phys(4);
let mut n0 = DepNode::new(0, alu_instr(0, 1, Some(r0), &[r1, r2]));
let n1 = DepNode::new(1, alu_instr(1, 1, Some(r3), &[r0, r4]));
let mut hr2 = hr.clone();
hr2.advance_to(0);
hr2.issue(&n0, 0);
let hazards = hr2.can_issue_at(&n1, &[n0.clone(), n1.clone()], 0);
assert!(!hazards.is_empty(), "N1 should have RAW hazard at cycle 0");
let hazards2 = hr2.can_issue_at(&n1, &[n0.clone(), n1.clone()], 1);
}
#[test]
fn test_hazard_recognizer_structural() {
let mut rm = ResourceModel::default_model();
rm.units.retain(|u| u.kind != FuncUnitKind::ALU);
rm.units.push(FuncUnit::new("ALU0", FuncUnitKind::ALU, 1));
let hr = HazardRecognizer::new(rm);
let r0 = Reg::Phys(0);
let r1 = Reg::Phys(1);
let n0 = DepNode::new(0, alu_instr(0, 1, Some(r0), &[]));
let n1 = DepNode::new(1, alu_instr(1, 1, Some(r1), &[]));
let mut hr2 = hr.clone();
hr2.advance_to(0);
hr2.issue(&n0, 0);
let hazards = hr2.can_issue_at(&n1, &[n0, n1.clone()], 0);
assert!(!hazards.is_empty(), "Structural hazard with single ALU");
}
#[test]
fn test_post_ra_scheduler_basic() {
let r0 = Reg::Phys(0);
let r1 = Reg::Phys(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
];
let mut post_ra = PostRAScheduler::new();
post_ra.break_anti_deps = false;
let result = post_ra.run(&instrs);
assert_eq!(result.len(), 2, "Should keep all instructions");
}
#[test]
fn test_post_ra_anti_dep_detection() {
let r0 = Reg::Phys(0);
let r1 = Reg::Phys(1);
let n0 = DepNode::new(0, alu_instr(0, 1, None, &[r0])); let n1 = DepNode::new(1, alu_instr(1, 1, Some(r0), &[r1]));
let post_ra = PostRAScheduler::new();
let anti_reg = post_ra.has_anti_dep(&n0, &n1);
assert_eq!(anti_reg, Some(0), "Anti-dep should be detected on r0");
}
#[test]
fn test_macro_fusion_cmp_jcc() {
let instrs = vec![
SchedInstr::new(0, 100).with_latency(1), SchedInstr::new(1, 200).with_latency(1).branch(), ];
let mf = MacroFusion::new();
let candidates = mf.analyze(&instrs);
assert!(!candidates.is_empty(), "Should detect CMP+JCC pattern");
assert_eq!(candidates[0].pattern, FusionPattern::CmpJcc);
assert!(candidates[0].eligible);
}
#[test]
fn test_macro_fusion_dec_jnz() {
let instrs = vec![
SchedInstr::new(0, 120).with_latency(1), SchedInstr::new(1, 200).with_latency(1).branch(), ];
let mf = MacroFusion::new();
let candidates = mf.analyze(&instrs);
assert!(!candidates.is_empty(), "Should detect DEC+JCC pattern");
assert_eq!(candidates[0].pattern, FusionPattern::DecJnz);
}
#[test]
fn test_macro_fusion_fuse_count() {
let instrs = vec![
SchedInstr::new(0, 100), SchedInstr::new(1, 200).branch(), SchedInstr::new(2, 110), SchedInstr::new(3, 201).branch(), ];
let mut mf = MacroFusion::new();
let mut instrs_mut = instrs.clone();
let fused = mf.fuse(&mut instrs_mut);
assert_eq!(fused, 2, "Should fuse both pairs");
assert_eq!(mf.fused_count, 2);
}
#[test]
fn test_macro_fusion_no_pattern() {
let instrs = vec![
SchedInstr::new(0, 1), SchedInstr::new(1, 200).branch(), ];
let mf = MacroFusion::new();
let candidates = mf.analyze(&instrs);
assert!(
candidates.is_empty(),
"No fusion without recognized pattern"
);
}
#[test]
fn test_macro_fusion_disable() {
let mut mf = MacroFusion::new();
mf.enabled = false;
let mut instrs = vec![SchedInstr::new(0, 100), SchedInstr::new(1, 200).branch()];
let fused = mf.fuse(&mut instrs);
assert_eq!(fused, 0, "No fusion when disabled");
}
#[test]
fn test_deep_scheduler_default() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
alu_instr(2, 1, Some(r2), &[r1]),
];
let result = DeepScheduler::run_default(&instrs);
assert_eq!(result.len(), 3);
}
#[test]
fn test_deep_scheduler_pre_ra() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
];
let result = DeepScheduler::run_pre_ra(&instrs);
assert_eq!(result.len(), 2);
}
#[test]
fn test_deep_scheduler_post_ra() {
let r0 = Reg::Phys(0);
let r1 = Reg::Phys(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[]), ];
let result = DeepScheduler::run_post_ra(&instrs);
assert_eq!(result.len(), 2);
}
#[test]
fn test_deep_scheduler_stats() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
];
let mut sched = DeepScheduler::new(DeepSchedConfig {
verbose: false,
..DeepSchedConfig::default()
});
sched.run(&instrs);
assert!(sched.stats.success, "Scheduling should succeed");
assert_eq!(sched.stats.num_instrs, 2);
assert!(sched.stats.true_deps > 0, "Should have true deps");
}
#[test]
fn test_deep_scheduler_fp_chain() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let r3 = Reg::Virt(3);
let instrs = vec![fp_add_instr(0, r0, r2, r3), fp_mul_instr(1, r1, r0, r2)];
let mut sched = DeepScheduler::new(DeepSchedConfig::default());
let result = sched.run(&instrs);
assert_eq!(result.len(), 2);
assert!(
sched.stats.schedule_length >= 5,
"FP chain should show latency"
);
}
#[test]
fn test_deep_scheduler_empty() {
let instrs: Vec<SchedInstr> = Vec::new();
let result = DeepScheduler::run_default(&instrs);
assert!(result.is_empty());
}
#[test]
fn test_deep_scheduler_single_instr() {
let r0 = Reg::Virt(0);
let instrs = vec![alu_instr(0, 1, Some(r0), &[])];
let result = DeepScheduler::run_default(&instrs);
assert_eq!(result.len(), 1);
}
#[test]
fn test_dep_graph_analysis_independent_groups() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let r3 = Reg::Virt(3);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
alu_instr(2, 1, Some(r2), &[]),
alu_instr(3, 1, Some(r3), &[r2]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let analysis = DepGraphAnalysis::analyze(&dg);
assert!(
analysis.independent_groups.len() >= 2,
"Should find independent groups"
);
assert!(analysis.max_parallelism >= 2);
}
#[test]
fn test_dep_graph_analysis_depth_sort() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
alu_instr(2, 1, Some(r2), &[r1]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let _dag = ScheduleDAG::new(dg.clone());
let sorted = DepGraphAnalysis::nodes_by_depth(&dg);
assert!(!sorted.is_empty(), "Should have sorted nodes");
assert_eq!(sorted[0], 0, "N0 should have highest depth");
}
#[test]
fn test_dep_graph_analysis_path_enumeration() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let r3 = Reg::Virt(3);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
alu_instr(2, 1, Some(r2), &[r0]),
alu_instr(3, 1, Some(r3), &[r1, r2]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let paths = DepGraphAnalysis::enumerate_paths(&dg, 0, 3, 10);
assert_eq!(paths.len(), 2, "Diamond should have 2 paths");
}
#[test]
fn test_force_directed_scheduler_converges() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[]),
alu_instr(2, 1, Some(r2), &[r0, r1]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let rm = ResourceModel::default_model();
let mut fds = ForceDirectedScheduler::new(dg, rm);
let schedule = fds.run(10);
assert_eq!(schedule.len(), 3);
let n0_cycle = schedule[&0];
let n1_cycle = schedule[&1];
let n2_cycle = schedule[&2];
assert!(n2_cycle >= n0_cycle + 1, "N2 must be after N0");
assert!(n2_cycle >= n1_cycle + 1, "N2 must be after N1");
}
#[test]
fn test_force_directed_scheduler_no_mobility() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let rm = ResourceModel::default_model();
let mut fds = ForceDirectedScheduler::new(dg, rm);
let schedule = fds.run(10);
assert_eq!(schedule.len(), 2);
assert_eq!(schedule[&1], schedule[&0] + 1);
}
#[test]
fn test_bidirectional_scheduler_top_down() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let rm = ResourceModel::default_model();
let mut bs = BidirectionalScheduler::new(dg, rm);
bs.direction = SchedDirection::TopDown;
let schedule = bs.run();
assert_eq!(schedule.len(), 2);
}
#[test]
fn test_bidirectional_scheduler_both_directions() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[]),
alu_instr(2, 1, Some(r2), &[r0, r1]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let rm = ResourceModel::default_model();
let mut bs = BidirectionalScheduler::new(dg, rm);
bs.direction = SchedDirection::Bidirectional;
let schedule = bs.run();
assert_eq!(schedule.len(), 3);
let (td_len, bu_len) = bs.comparison();
assert!(td_len > 0);
assert!(bu_len > 0);
}
#[test]
fn test_reg_liveness_simple() {
let r0 = Reg::Phys(0);
let r1 = Reg::Phys(1);
let r2 = Reg::Phys(2);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
alu_instr(2, 1, Some(r2), &[r1]),
];
let block_live_out = HashSet::new(); let liveness = RegLivenessAnalysis::compute(&instrs, &block_live_out);
assert!(
!liveness.is_dead_after(0, 0),
"r0 should be live after instruction 0"
);
assert!(
liveness.is_dead_after(1, 0),
"r0 should be dead after instruction 1"
);
assert!(
liveness.is_dead_after(2, 1),
"r1 should be dead after instruction 2"
);
}
#[test]
fn test_reg_liveness_pressure() {
let r0 = Reg::Phys(0);
let r1 = Reg::Phys(1);
let r2 = Reg::Phys(2);
let r3 = Reg::Phys(3);
let r4 = Reg::Phys(4);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[]),
alu_instr(2, 1, Some(r2), &[]),
alu_instr(3, 1, Some(r3), &[r0, r1, r2]),
alu_instr(4, 1, Some(r4), &[r3]),
];
let block_live_out = HashSet::new();
let liveness = RegLivenessAnalysis::compute(&instrs, &block_live_out);
let profile = liveness.pressure_profile();
assert!(
profile[2] >= 3,
"Pressure at instruction 2 should be at least 3"
);
assert!(liveness.max_pressure() >= 3);
}
#[test]
fn test_reg_liveness_find_free_reg() {
let r0 = Reg::Phys(0);
let r1 = Reg::Phys(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[]),
];
let block_live_out = HashSet::new();
let liveness = RegLivenessAnalysis::compute(&instrs, &block_live_out);
let free = liveness.find_free_reg(0, &[0, 2, 3]);
assert_eq!(free, Some(2), "r2 should be free at instruction 0");
}
#[test]
fn test_fusion_eligibility_cmp_jcc() {
let rules = FusionEligibility::cmp_jcc();
assert_eq!(rules.pattern, FusionPattern::CmpJcc);
assert!(rules.requires_flag_setting);
assert!(rules.allows_memory_operand);
}
#[test]
fn test_fusion_eligibility_check_memory() {
let rules = FusionEligibility::dec_jnz();
let first = SchedInstr::new(0, 120).load();
let second = SchedInstr::new(1, 200).branch();
let result = rules.check(&first, &second);
assert!(result.is_err(), "DEC+JNZ should reject memory operand");
}
#[test]
fn test_micro_op_fusion_basic() {
let mof = MicroOpFusion::new();
let uop1 = MicroOp::new(FuncUnitKind::ALU, 1);
let uop2 = MicroOp::new(FuncUnitKind::Branch, 1);
let fused = mof.try_fuse(&uop1, &uop2);
assert!(fused.is_some(), "ALU+Branch should fuse");
assert_eq!(fused.unwrap().unit, FuncUnitKind::ALU);
}
#[test]
fn test_micro_op_fusion_no_match() {
let mof = MicroOpFusion::new();
let uop1 = MicroOp::new(FuncUnitKind::FPAdd, 5);
let uop2 = MicroOp::new(FuncUnitKind::FPMul, 7);
let fused = mof.try_fuse(&uop1, &uop2);
assert!(fused.is_none(), "FPAdd+FPMul should not fuse");
}
#[test]
fn test_micro_op_fusion_decomposition() {
let mut mof = MicroOpFusion::new();
let uops = vec![
MicroOp::new(FuncUnitKind::ALU, 1),
MicroOp::new(FuncUnitKind::Branch, 1),
MicroOp::new(FuncUnitKind::ALU, 1),
];
let decomp = MicroOpDecomposition::new(42, uops);
let fused = mof.fuse_decomposition(&decomp);
assert_eq!(fused.count(), 2, "Should fuse first pair, leaving 2 uops");
assert_eq!(mof.fused_count, 1);
}
#[test]
fn test_instr_itinerary_total_cycles() {
let itinerary = InstrItinerary::new(100, "LOAD")
.with_stage(FuncUnitKind::Load, 3)
.with_stage(FuncUnitKind::ALU, 1);
assert_eq!(itinerary.total_cycles(), 4);
assert_eq!(itinerary.latency, 4);
assert!(itinerary.uses_unit(FuncUnitKind::Load));
assert!(itinerary.uses_unit(FuncUnitKind::ALU));
assert!(!itinerary.uses_unit(FuncUnitKind::FPAdd));
}
#[test]
fn test_micro_op_decomposition_latency() {
let uops = vec![
MicroOp::new(FuncUnitKind::ALU, 1),
MicroOp::new(FuncUnitKind::ALU, 1),
MicroOp::new(FuncUnitKind::Load, 3),
];
let decomp = MicroOpDecomposition::new(42, uops);
assert_eq!(decomp.count(), 3);
assert_eq!(decomp.total_latency, 5);
}
#[test]
fn test_sched_dag_mobility_zero_on_critical_path() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
alu_instr(2, 1, Some(r2), &[r1]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let dag = ScheduleDAG::new(dg);
for i in 0..3 {
assert_eq!(
dag.mobility(i),
0,
"Node {} on single chain should have zero mobility",
i
);
}
}
#[test]
fn test_sched_dag_estimate_schedule_length() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let dag = ScheduleDAG::new(dg);
let rm = ResourceModel::default_model();
let estimate = dag.estimate_schedule_length(&rm);
assert!(estimate >= 1, "Schedule length estimate should be positive");
}
#[test]
fn test_list_scheduler_balanced_priority() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let rm = ResourceModel::default_model();
let mut scheduler = ListScheduler::new(dg, rm);
scheduler.priority_heuristic = SchedPriority::Balanced;
let schedule = scheduler.run();
assert_eq!(schedule.len(), 2);
}
#[test]
fn test_list_scheduler_reg_pressure_priority() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let rm = ResourceModel::default_model();
let mut scheduler = ListScheduler::new(dg, rm);
scheduler.priority_heuristic = SchedPriority::RegPressure;
let schedule = scheduler.run();
assert_eq!(schedule.len(), 2);
}
#[test]
fn test_list_scheduler_no_resource_aware() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let r3 = Reg::Virt(3);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[]),
alu_instr(2, 1, Some(r2), &[]),
alu_instr(3, 1, Some(r3), &[]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let rm = ResourceModel::default_model();
let mut scheduler = ListScheduler::new(dg, rm);
scheduler.resource_aware = false;
scheduler.run();
assert!(
scheduler.schedule_length() <= 1,
"Without resources, all ops fit in 1 cycle"
);
}
#[test]
fn test_list_scheduler_display() {
let r0 = Reg::Virt(0);
let instrs = vec![alu_instr(0, 1, Some(r0), &[])];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let rm = ResourceModel::default_model();
let mut scheduler = ListScheduler::new(dg, rm);
scheduler.run();
let display = scheduler.display_schedule();
assert!(display.contains("ListScheduler Schedule"));
assert!(display.contains("N0"));
}
#[test]
fn test_deep_sched_config_default() {
let config = DeepSchedConfig::default();
assert_eq!(config.priority, SchedPriority::CriticalPath);
assert!(config.resource_aware);
assert!(config.macro_fusion);
assert_eq!(config.issue_width, 4);
}
#[test]
fn test_deep_scheduler_summary() {
let r0 = Reg::Virt(0);
let instrs = vec![alu_instr(0, 1, Some(r0), &[])];
let mut sched = DeepScheduler::default();
sched.run(&instrs);
let summary = sched.display_summary();
assert!(summary.contains("Deep Scheduling Pipeline Summary"));
assert!(summary.contains("Instructions: 1"));
}
#[test]
fn test_deep_scheduler_verbose_config() {
let config = DeepSchedConfig {
verbose: true,
..DeepSchedConfig::default()
};
assert!(config.verbose);
}
#[test]
fn test_hazard_recognizer_forwarding_path() {
let rm = ResourceModel::default_model();
let mut hr = HazardRecognizer::new(rm);
hr.add_forwarding(FuncUnitKind::ALU, FuncUnitKind::ALU, ForwardingPath::Full);
let r0 = Reg::Phys(0);
let r1 = Reg::Phys(1);
let n0 = DepNode::new(0, alu_instr(0, 1, Some(r0), &[]));
let n1 = DepNode::new(1, alu_instr(1, 1, Some(r1), &[r0]));
let mut hr2 = hr.clone();
hr2.issue(&n0, 0);
let hazards = hr2.can_issue_at(&n1, &[n0.clone(), n1.clone()], 0);
let has_raw = hazards.iter().any(|h| h.kind == HazardKind::ReadAfterWrite);
assert!(!has_raw, "Full forwarding should eliminate RAW hazard");
}
#[test]
fn test_hazard_recognizer_clear() {
let rm = ResourceModel::default_model();
let mut hr = HazardRecognizer::new(rm);
let r0 = Reg::Phys(0);
let n0 = DepNode::new(0, alu_instr(0, 1, Some(r0), &[]));
hr.issue(&n0, 5);
assert!(!hr.issued.is_empty());
hr.clear();
assert!(hr.issued.is_empty());
assert_eq!(hr.current_cycle, 0);
}
#[test]
fn test_hazard_kind_display() {
assert_eq!(HazardKind::ReadAfterWrite.as_str(), "RAW");
assert_eq!(HazardKind::Structural.as_str(), "structural");
assert_eq!(HazardKind::IssueWidth.as_str(), "issue_width");
}
#[test]
fn test_empty_graph_analysis() {
let dg = DependenceGraph::new();
let analysis = DepGraphAnalysis::analyze(&dg);
assert_eq!(analysis.independent_groups.len(), 0);
assert_eq!(analysis.max_parallelism, 0);
assert!(!analysis.has_cycle);
}
#[test]
fn test_single_node_schedule() {
let r0 = Reg::Virt(0);
let instrs = vec![alu_instr(0, 1, Some(r0), &[])];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let rm = ResourceModel::default_model();
let mut scheduler = ListScheduler::new(dg, rm);
let schedule = scheduler.run();
assert_eq!(schedule.len(), 1);
assert_eq!(schedule[0].1, 0, "Single instruction at cycle 0");
}
#[test]
fn test_many_independent_ops() {
let regs: Vec<Reg> = (0..16).map(|i| Reg::Virt(i)).collect();
let instrs: Vec<SchedInstr> = regs
.iter()
.enumerate()
.map(|(i, &r)| alu_instr(i, 1, Some(r), &[]))
.collect();
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let rm = ResourceModel::default_model();
let mut scheduler = ListScheduler::new(dg, rm);
let schedule = scheduler.run();
assert_eq!(schedule.len(), 16);
assert!(
scheduler.schedule_length() <= 4,
"16 ALU ops at issue width 4 should fit in ~4 cycles"
);
}
#[test]
fn test_diamond_dag_depth() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let rm = Reg::Virt(3);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
alu_instr(2, 1, Some(r2), &[r0]),
alu_instr(3, 1, Some(rm), &[r1, r2]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let dag = ScheduleDAG::new(dg);
let d0 = dag.graph().nodes[0].depth;
assert!(d0 > 0, "N0 should have positive depth");
let d3 = dag.graph().nodes[3].depth;
assert!(d3 < d0, "N3 should have smaller depth than N0");
}
#[test]
fn test_dep_node_earliest_start_no_preds() {
let instr = SchedInstr::new(0, 1);
let node = DepNode::new(0, instr);
let nodes = vec![node.clone()];
assert_eq!(node.earliest_start(&nodes), 0);
}
#[test]
fn test_dep_kind_properties() {
assert!(DepKind::True.is_true());
assert!(!DepKind::Anti.is_true());
assert!(DepKind::Anti.is_renamable());
assert!(DepKind::Output.is_renamable());
assert!(!DepKind::True.is_renamable());
assert!(DepKind::Memory.is_memory());
}
#[test]
fn test_reg_display() {
assert_eq!(format!("{}", Reg::Virt(5)), "v5");
assert_eq!(format!("{}", Reg::Phys(3)), "p3");
}
#[test]
fn test_sched_operand_equality() {
assert_eq!(
SchedOperand::Reg(Reg::Virt(1)),
SchedOperand::Reg(Reg::Virt(1))
);
assert_ne!(
SchedOperand::Reg(Reg::Virt(1)),
SchedOperand::Reg(Reg::Virt(2))
);
assert_eq!(SchedOperand::Imm(42), SchedOperand::Imm(42));
}
#[test]
fn test_resource_model_find_issue_slot() {
let mut rm = ResourceModel::default_model();
let alu_count = rm.get_unit_count(FuncUnitKind::ALU);
for _ in 0..alu_count {
rm.reserve(1, 0);
}
let slot = rm.find_issue_slot(1, 0);
assert_eq!(
slot, 1,
"ALU op should be scheduled at cycle 1 when cycle 0 is full"
);
}
#[test]
fn test_resource_model_can_issue_multiple() {
let rm = ResourceModel::default_model();
assert!(rm.can_issue(&[1, 1, 1, 1], 0));
assert!(!rm.can_issue(&[1, 1, 1, 1, 1], 0));
}
#[test]
fn test_resource_model_max_reserved() {
let mut rm = ResourceModel::default_model();
assert_eq!(rm.max_reserved_cycle(), None);
rm.reserve(1, 5);
assert_eq!(rm.max_reserved_cycle(), Some(5));
}
#[test]
fn test_scheduling_region_partition_empty() {
let instrs: Vec<SchedInstr> = Vec::new();
let regions = SchedulingRegion::partition(&instrs);
assert!(regions.is_empty());
}
#[test]
fn test_scheduling_region_partition_simple() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
];
let regions = SchedulingRegion::partition(&instrs);
assert_eq!(regions.len(), 1);
assert_eq!(regions[0].size, 2);
}
#[test]
fn test_scheduling_region_partition_with_branch() {
let r0 = Reg::Virt(0);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
SchedInstr::new(1, 7).with_latency(1).branch(),
alu_instr(2, 1, Some(r0), &[]),
];
let regions = SchedulingRegion::partition(&instrs);
assert!(regions.len() >= 2, "Branch should create region boundary");
assert!(regions.iter().any(|r| r.ends_with_branch));
}
#[test]
fn test_scheduling_region_extract() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
alu_instr(2, 1, Some(r0), &[]),
];
let region = SchedulingRegion::new(0, 2);
let slice = region.extract(&instrs);
assert_eq!(slice.len(), 2);
}
#[test]
fn test_schedule_verifier_valid_schedule() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let rm = ResourceModel::default_model();
let mut schedule = HashMap::new();
schedule.insert(0, 0);
schedule.insert(1, 1);
let verifier = ScheduleVerifier::new(dg, rm, schedule);
let result = verifier.verify();
assert!(
result.valid,
"Valid schedule should pass: {:?}",
result.violations
);
}
#[test]
fn test_schedule_verifier_violates_dependence() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let rm = ResourceModel::default_model();
let mut schedule = HashMap::new();
schedule.insert(0, 1);
schedule.insert(1, 0);
let verifier = ScheduleVerifier::new(dg, rm, schedule);
let result = verifier.verify();
assert!(!result.valid, "Invalid schedule should fail");
assert!(!result.violations.is_empty());
}
#[test]
fn test_schedule_verifier_missing_node() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[r0]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let rm = ResourceModel::default_model();
let mut schedule = HashMap::new();
schedule.insert(0, 0);
let verifier = ScheduleVerifier::new(dg, rm, schedule);
let result = verifier.verify();
assert!(!result.valid, "Missing node should be detected");
}
#[test]
fn test_schedule_verifier_resource_violation() {
let r0 = Reg::Virt(0);
let r1 = Reg::Virt(1);
let r2 = Reg::Virt(2);
let r3 = Reg::Virt(3);
let r4 = Reg::Virt(4);
let instrs = vec![
alu_instr(0, 1, Some(r0), &[]),
alu_instr(1, 1, Some(r1), &[]),
alu_instr(2, 1, Some(r2), &[]),
alu_instr(3, 1, Some(r3), &[]),
alu_instr(4, 1, Some(r4), &[]),
];
let mut dg = DependenceGraph::new();
dg.build(&instrs);
let rm = ResourceModel::default_model();
let mut schedule = HashMap::new();
for i in 0..5 {
schedule.insert(i, 0);
}
let verifier = ScheduleVerifier::new(dg, rm, schedule);
let result = verifier.verify();
assert!(!result.valid, "Should detect issue width violation");
}
#[test]
fn test_pipeline_stage_creation() {
let stage = PipelineStage::new(FuncUnitKind::ALU, 2);
assert_eq!(stage.cycles, 2);
assert!(stage.overlap);
let stage2 = PipelineStage::new(FuncUnitKind::FPDiv, 12).non_overlapping();
assert!(!stage2.overlap);
}
}