use llvm_native_core::codegen::{MachineBasicBlock, MachineFunction, MachineInstr, MachineOperand};
use llvm_native_core::machine_pipeliner::*;
use llvm_native_core::machine_scheduler::*;
use llvm_native_core::x86::x86_instr_info::X86Opcode;
use llvm_native_core::x86::x86_schedule_model::ProcResource;
use llvm_native_core::x86::x86_subtarget::X86SchedModel;
use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque};
use std::f64;
pub const DEFAULT_MAX_II: u32 = 200;
pub const DEFAULT_MAX_BACKTRACK: u32 = 100;
pub const MAX_PIPELINE_STAGES: u32 = 64;
pub const MIN_TRIP_COUNT_FOR_PIPELINING: u32 = 3;
pub const X86_LOOP_ALIGNMENT: u32 = 16;
pub const MAX_CODE_EXPANSION_FACTOR: f64 = 3.0;
pub const X86_MAX_PORTS: usize = 16;
pub const IMS_MAX_ITERATIONS: u32 = 100;
pub const DEFAULT_ISSUE_WIDTH: u32 = 4;
pub const MAX_PIPELINE_DEPTH: u32 = 32;
const STORE_OPCODE: u32 = 100;
fn is_alu_opcode(opcode: u32) -> bool {
matches!(
opcode,
2 | 3 | 6 | 7 | 8 | 20 | 21 | 23 | 50 | 9 | 10 | 51 | 52 | 53 | 54 | 55 | 56 | 57
)
}
fn is_multiply_opcode(opcode: u32) -> bool {
opcode == 4 || opcode == 48
}
fn is_divide_opcode(opcode: u32) -> bool {
opcode == 5 || opcode == 49
}
fn is_load_opcode(opcode: u32) -> bool {
opcode == 1 || opcode == 19 || opcode == 24 || opcode == 25
}
fn is_store_opcode(opcode: u32) -> bool {
opcode == STORE_OPCODE || opcode == 11
}
fn is_branch_opcode(opcode: u32) -> bool {
matches!(opcode, 13 | 14 | 15 | 16 | 17 | 58..=73)
}
fn is_compare_opcode(opcode: u32) -> bool {
opcode == 50 || opcode == 200 || opcode == 201
}
fn is_memory_opcode(opcode: u32) -> bool {
is_load_opcode(opcode) || is_store_opcode(opcode)
}
fn opcode_name(opcode: u32) -> &'static str {
match opcode {
0 => "NOP",
1 => "MOV/MOV_LOAD",
2 => "ADD",
3 => "SUB",
4 => "MUL",
5 => "DIV",
6 => "AND",
7 => "OR",
8 => "XOR",
9 => "SHL",
10 => "SHR",
11 => "PUSH",
12 => "POP",
13 => "CALL",
14 => "RET",
15 => "JMP",
16 => "JE",
17 => "JNE",
19 => "LEA",
20 => "INC",
21 => "DEC",
22 => "NOT",
23 => "NEG",
24 => "MOVSX",
25 => "MOVZX",
48 => "IMUL",
49 => "IDIV",
50 => "TEST",
51 => "SAR",
52 => "ROL",
53 => "ROR",
54 => "RCL",
55 => "RCR",
56 => "SHLD",
57 => "SHRD",
58..=71 => "Jcc",
72 => "LOOP",
73 => "LOOPE",
100 => "STORE",
_ => "UNKNOWN",
}
}
#[derive(Debug, Clone)]
pub struct X86PipelinerHooks {
pub sched_model: X86SchedModel,
pub issue_width: u32,
pub num_ports: u32,
pub num_resources: u32,
pub port_resources: Vec<u32>,
pub port_names: Vec<String>,
pub latency_table: HashMap<u32, u32>,
pub resource_table: HashMap<u32, u64>,
pub uop_table: HashMap<u32, u32>,
pub has_avx: bool,
pub has_avx2: bool,
pub has_avx512: bool,
pub supports_rotating_regs: bool,
pub loop_alignment: u32,
pub max_register_pressure: u32,
pub has_macro_fusion: bool,
pub has_micro_fusion: bool,
pub rob_size: u32,
pub load_buffer_size: u32,
pub store_buffer_size: u32,
pub scheduler_size: u32,
}
impl X86PipelinerHooks {
pub fn new(sched_model: X86SchedModel) -> Self {
let config = MicroArchConfig::for_model(sched_model);
let latency_table = Self::build_latency_table_for_config(&config);
let resource_table = Self::build_resource_table_for_config(&config);
let uop_table = Self::build_uop_table_for_config(&config);
X86PipelinerHooks {
sched_model,
issue_width: config.issue_width,
num_ports: config.num_ports,
num_resources: config.num_resources,
port_resources: config.port_resources.clone(),
port_names: config.port_names.clone(),
latency_table,
resource_table,
uop_table,
has_avx: config.has_avx,
has_avx2: config.has_avx2,
has_avx512: config.has_avx512,
supports_rotating_regs: true,
loop_alignment: config.loop_alignment,
max_register_pressure: 128,
has_macro_fusion: config.has_macro_fusion,
has_micro_fusion: config.has_micro_fusion,
rob_size: config.rob_size,
load_buffer_size: config.load_buffer_size,
store_buffer_size: config.store_buffer_size,
scheduler_size: config.scheduler_size,
}
}
fn build_latency_table_for_config(config: &MicroArchConfig) -> HashMap<u32, u32> {
let mut table = HashMap::new();
let alu_lat = config.alu_latency;
for op in &[0u32, 2, 3, 6, 7, 8, 20, 21, 23, 50, 22] {
table.insert(*op, alu_lat);
}
for op in &[9u32, 10, 51, 52, 53, 54, 55, 56, 57] {
table.insert(*op, 1);
}
table.insert(4, config.mul_latency);
table.insert(48, config.mul_latency);
table.insert(5, config.div_latency);
table.insert(49, config.div_latency);
table.insert(1, config.load_latency);
table.insert(19, config.load_latency); table.insert(24, config.load_latency); table.insert(25, config.load_latency); table.insert(STORE_OPCODE, config.store_latency);
table.insert(11, config.store_latency);
table.insert(12, config.store_latency);
for op in &[
13u32, 14, 15, 16, 17, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73,
] {
table.insert(*op, 1);
}
for op in 100..200 {
table.insert(op, config.fadd_latency);
}
for op in 200..300 {
table.insert(op, config.fmul_latency);
}
for op in 300..400 {
table.insert(op, config.fdiv_latency);
}
table
}
fn build_resource_table_for_config(config: &MicroArchConfig) -> HashMap<u32, u64> {
let mut table = HashMap::new();
let alu_ports = config.alu_port_mask;
let mul_port = config.mul_port_mask;
let div_port = config.div_port_mask;
let load_ports = config.load_port_mask;
let store_ports = config.store_port_mask;
let branch_port = config.branch_port_mask;
for op in &[2u32, 3, 6, 7, 8, 20, 21, 23, 50, 22] {
table.insert(*op, alu_ports);
}
for op in &[9u32, 10, 51, 52, 53, 54, 55, 56, 57] {
table.insert(*op, alu_ports);
}
table.insert(4, mul_port);
table.insert(48, mul_port);
table.insert(5, div_port);
table.insert(49, div_port);
table.insert(1, load_ports);
table.insert(19, load_ports);
table.insert(24, load_ports);
table.insert(25, load_ports);
table.insert(STORE_OPCODE, store_ports);
table.insert(11, store_ports);
for op in &[
13u32, 14, 15, 16, 17, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73,
] {
table.insert(*op, branch_port);
}
for op in 100..200 {
table.insert(op, config.fadd_port_mask);
}
for op in 200..300 {
table.insert(op, config.fmul_port_mask);
}
for op in 300..400 {
table.insert(op, config.fdiv_port_mask);
}
table
}
fn build_uop_table_for_config(config: &MicroArchConfig) -> HashMap<u32, u32> {
let mut table = HashMap::new();
for op in &[2u32, 3, 6, 7, 8, 20, 21, 23, 50, 9, 10, 22] {
table.insert(*op, 1);
}
for op in &[54u32, 55, 56, 57] {
table.insert(*op, 2);
}
table.insert(4, config.mul_uops);
table.insert(48, config.mul_uops);
table.insert(5, config.div_uops);
table.insert(49, config.div_uops);
table.insert(1, 1);
table.insert(19, 1);
table.insert(STORE_OPCODE, 2);
table.insert(15, 1);
table.insert(16, 1);
table.insert(17, 1);
table
}
pub fn get_latency(&self, opcode: u32) -> u32 {
*self.latency_table.get(&opcode).unwrap_or(&4)
}
pub fn get_resource_mask(&self, opcode: u32) -> u64 {
*self.resource_table.get(&opcode).unwrap_or(&0b0000_1111)
}
pub fn get_uop_count(&self, opcode: u32) -> u32 {
*self.uop_table.get(&opcode).unwrap_or(&1)
}
pub fn is_vector_opcode(&self, opcode: u32) -> bool {
(100..400).contains(&opcode) || (1000..3000).contains(&opcode)
}
pub fn get_vector_width(&self, opcode: u32) -> u32 {
if !self.is_vector_opcode(opcode) {
return 0;
}
if self.has_avx512 && opcode >= 2000 {
512
} else if self.has_avx2 && opcode >= 1000 || self.has_avx && (100..200).contains(&opcode) {
256
} else if (100..1000).contains(&opcode) {
128
} else {
0
}
}
pub fn estimate_register_pressure(&self, instrs: &[&MachineInstr]) -> u32 {
let mut live: HashSet<u32> = HashSet::new();
let mut max_live = 0u32;
for instr in instrs {
if let Some(def) = instr.def {
live.insert(def);
}
for op in &instr.operands {
if let MachineOperand::Reg(r) = op {
live.insert(*r);
}
}
max_live = max_live.max(live.len() as u32);
}
max_live
}
pub fn should_pipeline(
&self,
trip_count: u32,
body_size: usize,
has_carried_deps: bool,
) -> bool {
if trip_count < MIN_TRIP_COUNT_FOR_PIPELINING {
return false;
}
if body_size < 8 {
return false;
}
if has_carried_deps && trip_count < 8 {
return false;
}
if trip_count as usize * body_size < 50 {
return false;
}
true
}
pub fn get_loop_alignment(&self) -> u32 {
match self.sched_model {
X86SchedModel::SkylakeClient
| X86SchedModel::SkylakeServer
| X86SchedModel::IceLakeClient
| X86SchedModel::IceLakeServer
| X86SchedModel::AlderLake
| X86SchedModel::SapphireRapids
| X86SchedModel::GraniteRapids => 32,
_ => 16,
}
}
pub fn can_macro_fuse(&self, first_opcode: u32, second_opcode: u32) -> bool {
if !self.has_macro_fusion {
return false;
}
let first_is_compare = is_compare_opcode(first_opcode);
let second_is_branch = is_branch_opcode(second_opcode);
first_is_compare && second_is_branch
}
pub fn print_arch_info(&self) -> String {
format!(
"MicroArch: {:?}\n Issue width: {}\n Ports: {}\n Resources: {}\n \
ROB: {}\n Load buf: {}\n Store buf: {}\n Scheduler: {}\n \
AVX: {} AVX2: {} AVX-512: {}\n Macro-fusion: {} Micro-fusion: {}",
self.sched_model,
self.issue_width,
self.num_ports,
self.num_resources,
self.rob_size,
self.load_buffer_size,
self.store_buffer_size,
self.scheduler_size,
self.has_avx,
self.has_avx2,
self.has_avx512,
self.has_macro_fusion,
self.has_micro_fusion
)
}
}
impl Default for X86PipelinerHooks {
fn default() -> Self {
Self::new(X86SchedModel::Generic)
}
}
#[derive(Debug, Clone)]
struct MicroArchConfig {
issue_width: u32,
num_ports: u32,
num_resources: u32,
port_resources: Vec<u32>,
port_names: Vec<String>,
alu_latency: u32,
mul_latency: u32,
div_latency: u32,
load_latency: u32,
store_latency: u32,
fadd_latency: u32,
fmul_latency: u32,
fdiv_latency: u32,
mul_uops: u32,
div_uops: u32,
alu_port_mask: u64,
mul_port_mask: u64,
div_port_mask: u64,
load_port_mask: u64,
store_port_mask: u64,
branch_port_mask: u64,
fadd_port_mask: u64,
fmul_port_mask: u64,
fdiv_port_mask: u64,
has_avx: bool,
has_avx2: bool,
has_avx512: bool,
has_macro_fusion: bool,
has_micro_fusion: bool,
loop_alignment: u32,
rob_size: u32,
load_buffer_size: u32,
store_buffer_size: u32,
scheduler_size: u32,
}
impl MicroArchConfig {
fn for_model(model: X86SchedModel) -> Self {
match model {
X86SchedModel::SandyBridge => MicroArchConfig {
issue_width: 4,
num_ports: 6,
num_resources: 6,
port_resources: vec![1, 1, 1, 1, 1, 1],
port_names: vec![
"Port0".into(),
"Port1".into(),
"Port2".into(),
"Port3".into(),
"Port4".into(),
"Port5".into(),
],
alu_latency: 1,
mul_latency: 3,
div_latency: 30,
load_latency: 4,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 5,
fdiv_latency: 14,
mul_uops: 1,
div_uops: 25,
alu_port_mask: 0b0011_0011, mul_port_mask: 0b0000_0001, div_port_mask: 0b0000_0001, load_port_mask: 0b0000_1100, store_port_mask: 0b0001_1100, branch_port_mask: 0b0010_0000, fadd_port_mask: 0b0000_0001, fmul_port_mask: 0b0000_0001, fdiv_port_mask: 0b0000_0001, has_avx: true,
has_avx2: false,
has_avx512: false,
has_macro_fusion: true,
has_micro_fusion: false,
loop_alignment: 16,
rob_size: 168,
load_buffer_size: 64,
store_buffer_size: 36,
scheduler_size: 54,
},
X86SchedModel::IvyBridge => MicroArchConfig {
issue_width: 4,
num_ports: 6,
num_resources: 6,
port_resources: vec![1, 1, 1, 1, 1, 1],
port_names: vec![
"Port0".into(),
"Port1".into(),
"Port2".into(),
"Port3".into(),
"Port4".into(),
"Port5".into(),
],
alu_latency: 1,
mul_latency: 3,
div_latency: 28,
load_latency: 4,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 5,
fdiv_latency: 14,
mul_uops: 1,
div_uops: 22,
alu_port_mask: 0b0011_0011,
mul_port_mask: 0b0000_0001,
div_port_mask: 0b0000_0001,
load_port_mask: 0b0000_1100,
store_port_mask: 0b0001_1100,
branch_port_mask: 0b0010_0000,
fadd_port_mask: 0b0000_0001,
fmul_port_mask: 0b0000_0001,
fdiv_port_mask: 0b0000_0001,
has_avx: true,
has_avx2: false,
has_avx512: false,
has_macro_fusion: true,
has_micro_fusion: false,
loop_alignment: 16,
rob_size: 168,
load_buffer_size: 64,
store_buffer_size: 36,
scheduler_size: 54,
},
X86SchedModel::Haswell => MicroArchConfig {
issue_width: 4,
num_ports: 8,
num_resources: 8,
port_resources: vec![1, 1, 1, 1, 1, 1, 1, 1],
port_names: vec![
"Port0".into(),
"Port1".into(),
"Port2".into(),
"Port3".into(),
"Port4".into(),
"Port5".into(),
"Port6".into(),
"Port7".into(),
],
alu_latency: 1,
mul_latency: 3,
div_latency: 30,
load_latency: 4,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 5,
fdiv_latency: 14,
mul_uops: 1,
div_uops: 25,
alu_port_mask: 0b0110_0011, mul_port_mask: 0b0000_0001, div_port_mask: 0b0000_0001, load_port_mask: 0b1000_1100, store_port_mask: 0b1001_1100, branch_port_mask: 0b0010_0000, fadd_port_mask: 0b0000_0001, fmul_port_mask: 0b0000_0011, fdiv_port_mask: 0b0000_0001, has_avx: true,
has_avx2: true,
has_avx512: false,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 16,
rob_size: 192,
load_buffer_size: 72,
store_buffer_size: 42,
scheduler_size: 60,
},
X86SchedModel::Broadwell => MicroArchConfig {
issue_width: 4,
num_ports: 8,
num_resources: 8,
port_resources: vec![1, 1, 1, 1, 1, 1, 1, 1],
port_names: vec![
"Port0".into(),
"Port1".into(),
"Port2".into(),
"Port3".into(),
"Port4".into(),
"Port5".into(),
"Port6".into(),
"Port7".into(),
],
alu_latency: 1,
mul_latency: 3,
div_latency: 30,
load_latency: 4,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 5,
fdiv_latency: 14,
mul_uops: 1,
div_uops: 25,
alu_port_mask: 0b0110_0011,
mul_port_mask: 0b0000_0001,
div_port_mask: 0b0000_0001,
load_port_mask: 0b1000_1100,
store_port_mask: 0b1001_1100,
branch_port_mask: 0b0010_0000,
fadd_port_mask: 0b0000_0001,
fmul_port_mask: 0b0000_0011,
fdiv_port_mask: 0b0000_0001,
has_avx: true,
has_avx2: true,
has_avx512: false,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 16,
rob_size: 192,
load_buffer_size: 72,
store_buffer_size: 42,
scheduler_size: 60,
},
X86SchedModel::SkylakeClient => MicroArchConfig {
issue_width: 4,
num_ports: 8,
num_resources: 8,
port_resources: vec![1, 1, 1, 1, 1, 1, 1, 1],
port_names: vec![
"Port0".into(),
"Port1".into(),
"Port2".into(),
"Port3".into(),
"Port4".into(),
"Port5".into(),
"Port6".into(),
"Port7".into(),
],
alu_latency: 1,
mul_latency: 3,
div_latency: 30,
load_latency: 4,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 4,
fdiv_latency: 12,
mul_uops: 1,
div_uops: 25,
alu_port_mask: 0b0110_0011, mul_port_mask: 0b0000_0001, div_port_mask: 0b0000_0001, load_port_mask: 0b0000_1100, store_port_mask: 0b0001_1100, branch_port_mask: 0b0010_0000, fadd_port_mask: 0b0010_0001, fmul_port_mask: 0b0000_0011, fdiv_port_mask: 0b0000_0001, has_avx: true,
has_avx2: true,
has_avx512: false,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 224,
load_buffer_size: 72,
store_buffer_size: 56,
scheduler_size: 97,
},
X86SchedModel::SkylakeServer => MicroArchConfig {
issue_width: 4,
num_ports: 8,
num_resources: 8,
port_resources: vec![1, 1, 1, 1, 1, 1, 1, 1],
port_names: vec![
"Port0".into(),
"Port1".into(),
"Port2".into(),
"Port3".into(),
"Port4".into(),
"Port5".into(),
"Port6".into(),
"Port7".into(),
],
alu_latency: 1,
mul_latency: 3,
div_latency: 30,
load_latency: 4,
store_latency: 1,
fadd_latency: 4,
fmul_latency: 4,
fdiv_latency: 14,
mul_uops: 1,
div_uops: 25,
alu_port_mask: 0b0110_0011,
mul_port_mask: 0b0000_0001,
div_port_mask: 0b0000_0001,
load_port_mask: 0b0000_1100,
store_port_mask: 0b0001_1100,
branch_port_mask: 0b0010_0000,
fadd_port_mask: 0b0010_0001,
fmul_port_mask: 0b0000_0011,
fdiv_port_mask: 0b0000_0001,
has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 224,
load_buffer_size: 72,
store_buffer_size: 56,
scheduler_size: 97,
},
X86SchedModel::CascadeLake => MicroArchConfig {
issue_width: 4,
num_ports: 8,
num_resources: 8,
port_resources: vec![1; 8],
port_names: (0..8).map(|i| format!("Port{}", i)).collect(),
alu_latency: 1,
mul_latency: 3,
div_latency: 30,
load_latency: 4,
store_latency: 1,
fadd_latency: 4,
fmul_latency: 4,
fdiv_latency: 14,
mul_uops: 1,
div_uops: 25,
alu_port_mask: 0b0110_0011,
mul_port_mask: 0b0000_0001,
div_port_mask: 0b0000_0001,
load_port_mask: 0b0000_1100,
store_port_mask: 0b0001_1100,
branch_port_mask: 0b0010_0000,
fadd_port_mask: 0b0010_0001,
fmul_port_mask: 0b0000_0011,
fdiv_port_mask: 0b0000_0001,
has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 224,
load_buffer_size: 72,
store_buffer_size: 56,
scheduler_size: 97,
},
X86SchedModel::IceLakeClient => MicroArchConfig {
issue_width: 5,
num_ports: 10,
num_resources: 10,
port_resources: vec![1; 10],
port_names: (0..10).map(|i| format!("Port{}", i)).collect(),
alu_latency: 1,
mul_latency: 3,
div_latency: 14,
load_latency: 5,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 4,
fdiv_latency: 11,
mul_uops: 1,
div_uops: 12,
alu_port_mask: 0b0000_0110_0011, mul_port_mask: 0b0000_0000_0001, div_port_mask: 0b0000_0000_0001, load_port_mask: 0b0000_0011_1100_0000, store_port_mask: 0b0000_0011_1001_0000, branch_port_mask: 0b0000_0000_0010_0000, fadd_port_mask: 0b0000_0000_0010_0001, fmul_port_mask: 0b0000_0000_0000_0011, fdiv_port_mask: 0b0000_0000_0000_0001, has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 352,
load_buffer_size: 128,
store_buffer_size: 72,
scheduler_size: 160,
},
X86SchedModel::IceLakeServer => MicroArchConfig {
issue_width: 5,
num_ports: 10,
num_resources: 10,
port_resources: vec![1; 10],
port_names: (0..10).map(|i| format!("Port{}", i)).collect(),
alu_latency: 1,
mul_latency: 3,
div_latency: 14,
load_latency: 5,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 4,
fdiv_latency: 11,
mul_uops: 1,
div_uops: 12,
alu_port_mask: 0b0000_0110_0011,
mul_port_mask: 0b0000_0000_0001,
div_port_mask: 0b0000_0000_0001,
load_port_mask: 0b0000_0011_1100_0000,
store_port_mask: 0b0000_0011_1001_0000,
branch_port_mask: 0b0000_0000_0010_0000,
fadd_port_mask: 0b0000_0000_0010_0001,
fmul_port_mask: 0b0000_0000_0000_0011,
fdiv_port_mask: 0b0000_0000_0000_0001,
has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 352,
load_buffer_size: 128,
store_buffer_size: 72,
scheduler_size: 160,
},
X86SchedModel::TigerLake => MicroArchConfig {
issue_width: 5,
num_ports: 10,
num_resources: 10,
port_resources: vec![1; 10],
port_names: (0..10).map(|i| format!("Port{}", i)).collect(),
alu_latency: 1,
mul_latency: 3,
div_latency: 14,
load_latency: 5,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 4,
fdiv_latency: 11,
mul_uops: 1,
div_uops: 12,
alu_port_mask: 0b0000_0110_0011,
mul_port_mask: 0b0000_0000_0001,
div_port_mask: 0b0000_0000_0001,
load_port_mask: 0b0000_0011_1100_0000,
store_port_mask: 0b0000_0011_1001_0000,
branch_port_mask: 0b0000_0000_0010_0000,
fadd_port_mask: 0b0000_0000_0010_0001,
fmul_port_mask: 0b0000_0000_0000_0011,
fdiv_port_mask: 0b0000_0000_0000_0001,
has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 352,
load_buffer_size: 128,
store_buffer_size: 72,
scheduler_size: 160,
},
X86SchedModel::SapphireRapids => MicroArchConfig {
issue_width: 6,
num_ports: 12,
num_resources: 12,
port_resources: vec![1; 12],
port_names: (0..12).map(|i| format!("Port{}", i)).collect(),
alu_latency: 1,
mul_latency: 3,
div_latency: 14,
load_latency: 5,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 4,
fdiv_latency: 11,
mul_uops: 1,
div_uops: 12,
alu_port_mask: 0b0000_0110_0000_0011,
mul_port_mask: 0b0000_0000_0000_0001,
div_port_mask: 0b0000_0000_0000_0001,
load_port_mask: 0b0000_0011_1100_0000,
store_port_mask: 0b0000_0111_1001_0000,
branch_port_mask: 0b0000_0000_0010_0000,
fadd_port_mask: 0b0000_0000_0010_0001,
fmul_port_mask: 0b0000_0000_0010_0011,
fdiv_port_mask: 0b0000_0000_0000_0001,
has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 512,
load_buffer_size: 192,
store_buffer_size: 114,
scheduler_size: 200,
},
X86SchedModel::EmeraldRapids => MicroArchConfig {
issue_width: 6,
num_ports: 12,
num_resources: 12,
port_resources: vec![1; 12],
port_names: (0..12).map(|i| format!("Port{}", i)).collect(),
alu_latency: 1,
mul_latency: 3,
div_latency: 14,
load_latency: 5,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 4,
fdiv_latency: 11,
mul_uops: 1,
div_uops: 12,
alu_port_mask: 0b0000_0110_0000_0011,
mul_port_mask: 0b0000_0000_0000_0001,
div_port_mask: 0b0000_0000_0000_0001,
load_port_mask: 0b0000_0011_1100_0000,
store_port_mask: 0b0000_0111_1001_0000,
branch_port_mask: 0b0000_0000_0010_0000,
fadd_port_mask: 0b0000_0000_0010_0001,
fmul_port_mask: 0b0000_0000_0010_0011,
fdiv_port_mask: 0b0000_0000_0000_0001,
has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 512,
load_buffer_size: 192,
store_buffer_size: 114,
scheduler_size: 200,
},
X86SchedModel::GraniteRapids => MicroArchConfig {
issue_width: 8,
num_ports: 14,
num_resources: 14,
port_resources: vec![1; 14],
port_names: (0..14).map(|i| format!("Port{}", i)).collect(),
alu_latency: 1,
mul_latency: 2,
div_latency: 12,
load_latency: 5,
store_latency: 1,
fadd_latency: 2,
fmul_latency: 3,
fdiv_latency: 10,
mul_uops: 1,
div_uops: 10,
alu_port_mask: 0b0000_0110_0000_0011,
mul_port_mask: 0b0000_0000_0000_0001,
div_port_mask: 0b0000_0000_0000_0001,
load_port_mask: 0b0000_1111_1100_0000,
store_port_mask: 0b0000_1111_1001_0000,
branch_port_mask: 0b0000_0000_0010_0000,
fadd_port_mask: 0b0000_0000_0010_0001,
fmul_port_mask: 0b0000_0000_0010_0011,
fdiv_port_mask: 0b0000_0000_0000_0001,
has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 640,
load_buffer_size: 256,
store_buffer_size: 144,
scheduler_size: 280,
},
X86SchedModel::AlderLake => MicroArchConfig {
issue_width: 6,
num_ports: 12,
num_resources: 12,
port_resources: vec![1; 12],
port_names: (0..12).map(|i| format!("Port{}", i)).collect(),
alu_latency: 1,
mul_latency: 3,
div_latency: 14,
load_latency: 5,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 4,
fdiv_latency: 11,
mul_uops: 1,
div_uops: 12,
alu_port_mask: 0b0000_0011_0000_0011,
mul_port_mask: 0b0000_0000_0000_0001,
div_port_mask: 0b0000_0000_0000_0001,
load_port_mask: 0b0000_1111_0000_0000,
store_port_mask: 0b0000_1111_0001_0000,
branch_port_mask: 0b0000_0000_0010_0000,
fadd_port_mask: 0b0000_0000_0010_0001,
fmul_port_mask: 0b0000_0000_0010_0011,
fdiv_port_mask: 0b0000_0000_0000_0001,
has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 512,
load_buffer_size: 192,
store_buffer_size: 114,
scheduler_size: 200,
},
X86SchedModel::RocketLake => MicroArchConfig {
issue_width: 5,
num_ports: 10,
num_resources: 10,
port_resources: vec![1; 10],
port_names: (0..10).map(|i| format!("Port{}", i)).collect(),
alu_latency: 1,
mul_latency: 3,
div_latency: 30,
load_latency: 5,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 4,
fdiv_latency: 12,
mul_uops: 1,
div_uops: 25,
alu_port_mask: 0b0000_0110_0011,
mul_port_mask: 0b0000_0000_0001,
div_port_mask: 0b0000_0000_0001,
load_port_mask: 0b0000_0011_1100_0000,
store_port_mask: 0b0000_0011_1001_0000,
branch_port_mask: 0b0000_0000_0010_0000,
fadd_port_mask: 0b0000_0000_0010_0001,
fmul_port_mask: 0b0000_0000_0000_0011,
fdiv_port_mask: 0b0000_0000_0000_0001,
has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 352,
load_buffer_size: 128,
store_buffer_size: 72,
scheduler_size: 160,
},
X86SchedModel::MeteorLake => MicroArchConfig {
issue_width: 8,
num_ports: 14,
num_resources: 14,
port_resources: vec![1; 14],
port_names: (0..14).map(|i| format!("Port{}", i)).collect(),
alu_latency: 1,
mul_latency: 2,
div_latency: 12,
load_latency: 5,
store_latency: 1,
fadd_latency: 2,
fmul_latency: 3,
fdiv_latency: 10,
mul_uops: 1,
div_uops: 10,
alu_port_mask: 0b0000_0011_0000_0011,
mul_port_mask: 0b0000_0000_0000_0001,
div_port_mask: 0b0000_0000_0000_0001,
load_port_mask: 0b0011_1111_0000_0000,
store_port_mask: 0b0011_1111_0001_0000,
branch_port_mask: 0b0000_0000_0010_0000,
fadd_port_mask: 0b0000_0000_0010_0001,
fmul_port_mask: 0b0000_0000_0010_0011,
fdiv_port_mask: 0b0000_0000_0000_0001,
has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 640,
load_buffer_size: 256,
store_buffer_size: 144,
scheduler_size: 280,
},
X86SchedModel::ArrowLake => MicroArchConfig {
issue_width: 8,
num_ports: 14,
num_resources: 14,
port_resources: vec![1; 14],
port_names: (0..14).map(|i| format!("Port{}", i)).collect(),
alu_latency: 1,
mul_latency: 2,
div_latency: 12,
load_latency: 5,
store_latency: 1,
fadd_latency: 2,
fmul_latency: 3,
fdiv_latency: 10,
mul_uops: 1,
div_uops: 10,
alu_port_mask: 0b0000_0011_0000_0011,
mul_port_mask: 0b0000_0000_0000_0001,
div_port_mask: 0b0000_0000_0000_0001,
load_port_mask: 0b0011_1111_0000_0000,
store_port_mask: 0b0011_1111_0001_0000,
branch_port_mask: 0b0000_0000_0010_0000,
fadd_port_mask: 0b0000_0000_0010_0001,
fmul_port_mask: 0b0000_0000_0010_0011,
fdiv_port_mask: 0b0000_0000_0000_0001,
has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 640,
load_buffer_size: 256,
store_buffer_size: 144,
scheduler_size: 280,
},
X86SchedModel::LunarLake => MicroArchConfig {
issue_width: 8,
num_ports: 14,
num_resources: 14,
port_resources: vec![1; 14],
port_names: (0..14).map(|i| format!("Port{}", i)).collect(),
alu_latency: 1,
mul_latency: 2,
div_latency: 12,
load_latency: 5,
store_latency: 1,
fadd_latency: 2,
fmul_latency: 3,
fdiv_latency: 10,
mul_uops: 1,
div_uops: 10,
alu_port_mask: 0b0000_0011_0000_0011,
mul_port_mask: 0b0000_0000_0000_0001,
div_port_mask: 0b0000_0000_0000_0001,
load_port_mask: 0b0011_1111_0000_0000,
store_port_mask: 0b0011_1111_0001_0000,
branch_port_mask: 0b0000_0000_0010_0000,
fadd_port_mask: 0b0000_0000_0010_0001,
fmul_port_mask: 0b0000_0000_0010_0011,
fdiv_port_mask: 0b0000_0000_0000_0001,
has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: true,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 640,
load_buffer_size: 256,
store_buffer_size: 144,
scheduler_size: 280,
},
X86SchedModel::Zen1 => MicroArchConfig {
issue_width: 4,
num_ports: 10,
num_resources: 10,
port_resources: vec![1; 10],
port_names: vec![
"ALU0".into(),
"ALU1".into(),
"ALU2".into(),
"ALU3".into(),
"AGU0".into(),
"AGU1".into(),
"FPU0".into(),
"FPU1".into(),
"FPU2".into(),
"FPU3".into(),
],
alu_latency: 1,
mul_latency: 3,
div_latency: 14,
load_latency: 4,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 3,
fdiv_latency: 10,
mul_uops: 1,
div_uops: 12,
alu_port_mask: 0b0000_0000_1111, mul_port_mask: 0b0000_0000_0001, div_port_mask: 0b0000_0000_0001, load_port_mask: 0b0000_0011_0000, store_port_mask: 0b0000_0011_0000, branch_port_mask: 0b0000_0000_0001, fadd_port_mask: 0b0000_1111_0000,
fmul_port_mask: 0b0000_0011_0000,
fdiv_port_mask: 0b0000_0001_0000,
has_avx: true,
has_avx2: false,
has_avx512: false,
has_macro_fusion: false,
has_micro_fusion: false,
loop_alignment: 16,
rob_size: 192,
load_buffer_size: 72,
store_buffer_size: 44,
scheduler_size: 84,
},
X86SchedModel::Zen2 => MicroArchConfig {
issue_width: 4,
num_ports: 10,
num_resources: 10,
port_resources: vec![1; 10],
port_names: vec![
"ALU0".into(),
"ALU1".into(),
"ALU2".into(),
"ALU3".into(),
"AGU0".into(),
"AGU1".into(),
"AGU2".into(),
"FPU0".into(),
"FPU1".into(),
"FPU2".into(),
],
alu_latency: 1,
mul_latency: 3,
div_latency: 14,
load_latency: 4,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 3,
fdiv_latency: 10,
mul_uops: 1,
div_uops: 12,
alu_port_mask: 0b0000_0000_1111,
mul_port_mask: 0b0000_0000_0001,
div_port_mask: 0b0000_0000_0001,
load_port_mask: 0b0000_0111_0000,
store_port_mask: 0b0000_0111_0000,
branch_port_mask: 0b0000_0000_0001,
fadd_port_mask: 0b0000_0111_0000,
fmul_port_mask: 0b0000_0011_0000,
fdiv_port_mask: 0b0000_0001_0000,
has_avx: true,
has_avx2: false,
has_avx512: false,
has_macro_fusion: false,
has_micro_fusion: false,
loop_alignment: 16,
rob_size: 224,
load_buffer_size: 72,
store_buffer_size: 48,
scheduler_size: 92,
},
X86SchedModel::Zen3 => MicroArchConfig {
issue_width: 6,
num_ports: 11,
num_resources: 11,
port_resources: vec![1; 11],
port_names: vec![
"ALU0".into(),
"ALU1".into(),
"ALU2".into(),
"ALU3".into(),
"AGU0".into(),
"AGU1".into(),
"AGU2".into(),
"FPU0".into(),
"FPU1".into(),
"FPU2".into(),
"FPU3".into(),
],
alu_latency: 1,
mul_latency: 3,
div_latency: 14,
load_latency: 4,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 3,
fdiv_latency: 10,
mul_uops: 1,
div_uops: 12,
alu_port_mask: 0b0000_0000_1111,
mul_port_mask: 0b0000_0000_0001,
div_port_mask: 0b0000_0000_0001,
load_port_mask: 0b0000_0111_0000,
store_port_mask: 0b0000_0111_0000,
branch_port_mask: 0b0000_0000_0001,
fadd_port_mask: 0b0000_1111_0000_0000,
fmul_port_mask: 0b0000_0011_0000_0000,
fdiv_port_mask: 0b0000_0001_0000_0000,
has_avx: true,
has_avx2: false,
has_avx512: false,
has_macro_fusion: false,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 256,
load_buffer_size: 72,
store_buffer_size: 64,
scheduler_size: 100,
},
X86SchedModel::Zen4 => MicroArchConfig {
issue_width: 6,
num_ports: 13,
num_resources: 13,
port_resources: vec![1; 13],
port_names: vec![
"ALU0".into(),
"ALU1".into(),
"ALU2".into(),
"ALU3".into(),
"AGU0".into(),
"AGU1".into(),
"AGU2".into(),
"FPU0".into(),
"FPU1".into(),
"FPU2".into(),
"FPU3".into(),
"Branch0".into(),
"Branch1".into(),
],
alu_latency: 1,
mul_latency: 3,
div_latency: 14,
load_latency: 4,
store_latency: 1,
fadd_latency: 3,
fmul_latency: 3,
fdiv_latency: 10,
mul_uops: 1,
div_uops: 12,
alu_port_mask: 0b0000_0000_1111,
mul_port_mask: 0b0000_0000_0001,
div_port_mask: 0b0000_0000_0001,
load_port_mask: 0b0000_0111_0000,
store_port_mask: 0b0000_0111_0000,
branch_port_mask: 0b0000_0000_0001,
fadd_port_mask: 0b0000_1111_0000_0000,
fmul_port_mask: 0b0000_0011_0000_0000,
fdiv_port_mask: 0b0000_0001_0000_0000,
has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: false,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 320,
load_buffer_size: 88,
store_buffer_size: 72,
scheduler_size: 128,
},
X86SchedModel::Zen5 => MicroArchConfig {
issue_width: 8,
num_ports: 15,
num_resources: 15,
port_resources: vec![1; 15],
port_names: vec![
"ALU0".into(),
"ALU1".into(),
"ALU2".into(),
"ALU3".into(),
"ALU4".into(),
"AGU0".into(),
"AGU1".into(),
"AGU2".into(),
"AGU3".into(),
"FPU0".into(),
"FPU1".into(),
"FPU2".into(),
"FPU3".into(),
"Branch0".into(),
"Branch1".into(),
],
alu_latency: 1,
mul_latency: 2,
div_latency: 12,
load_latency: 4,
store_latency: 1,
fadd_latency: 2,
fmul_latency: 2,
fdiv_latency: 9,
mul_uops: 1,
div_uops: 10,
alu_port_mask: 0b0000_0001_1111,
mul_port_mask: 0b0000_0000_0001,
div_port_mask: 0b0000_0000_0001,
load_port_mask: 0b0000_1111_0000_0000,
store_port_mask: 0b0000_1111_0000_0000,
branch_port_mask: 0b0000_0000_0001,
fadd_port_mask: 0b0011_1100_0000_0000,
fmul_port_mask: 0b0001_1100_0000_0000,
fdiv_port_mask: 0b0000_0100_0000_0000,
has_avx: true,
has_avx2: true,
has_avx512: true,
has_macro_fusion: false,
has_micro_fusion: true,
loop_alignment: 32,
rob_size: 448,
load_buffer_size: 120,
store_buffer_size: 96,
scheduler_size: 160,
},
_ => MicroArchConfig {
issue_width: 4,
num_ports: 8,
num_resources: 8,
port_resources: vec![1; 8],
port_names: (0..8).map(|i| format!("Port{}", i)).collect(),
alu_latency: 1,
mul_latency: 4,
div_latency: 40,
load_latency: 4,
store_latency: 1,
fadd_latency: 4,
fmul_latency: 5,
fdiv_latency: 20,
mul_uops: 1,
div_uops: 30,
alu_port_mask: 0b0000_1111,
mul_port_mask: 0b0000_0001,
div_port_mask: 0b0000_0001,
load_port_mask: 0b0011_1100,
store_port_mask: 0b0011_1100,
branch_port_mask: 0b0100_0000,
fadd_port_mask: 0b0000_0011,
fmul_port_mask: 0b0000_0011,
fdiv_port_mask: 0b0000_0001,
has_avx: false,
has_avx2: false,
has_avx512: false,
has_macro_fusion: false,
has_micro_fusion: false,
loop_alignment: 16,
rob_size: 168,
load_buffer_size: 64,
store_buffer_size: 36,
scheduler_size: 54,
},
}
}
}
#[derive(Debug, Clone)]
pub struct NaturalLoop {
pub header: usize,
pub blocks: BTreeSet<usize>,
pub back_edges: Vec<(usize, usize)>,
pub preheader: Option<usize>,
pub exit_blocks: BTreeSet<usize>,
pub latch_blocks: BTreeSet<usize>,
pub depth: u32,
pub nesting_level: u32,
pub parent_loop: Option<usize>,
pub child_loops: Vec<usize>,
}
#[derive(Debug, Clone)]
pub struct InductionVariable {
pub reg: u32,
pub base: i64,
pub step: i64,
pub is_primary: bool,
pub is_derived: bool,
pub derived_from: Option<u32>,
pub end_reg: Option<u32>,
pub counts_up: bool,
pub is_exit_condition: bool,
pub def_instr_idx: usize,
pub def_block_idx: usize,
}
#[derive(Debug, Clone)]
pub struct MemDep {
pub from: usize,
pub to: usize,
pub must_alias: bool,
pub may_alias: bool,
pub same_base: bool,
pub distance: i32,
pub from_block: usize,
pub to_block: usize,
pub from_is_load: bool,
pub to_is_load: bool,
}
#[derive(Debug, Clone)]
pub struct X86LoopAnalysis {
pub loops: Vec<NaturalLoop>,
pub induction_vars: HashMap<usize, Vec<InductionVariable>>,
pub memory_deps: HashMap<usize, Vec<MemDep>>,
pub trip_counts: HashMap<usize, u32>,
pub iter_bounds: HashMap<usize, (i64, i64)>,
pub carried_deps: HashMap<usize, Vec<LoopCarriedDep>>,
pub dominators: HashMap<usize, BTreeSet<usize>>,
pub post_dominators: HashMap<usize, BTreeSet<usize>>,
pub cfg: HashMap<usize, Vec<usize>>,
pub reverse_cfg: HashMap<usize, Vec<usize>>,
}
impl X86LoopAnalysis {
pub fn new() -> Self {
X86LoopAnalysis {
loops: Vec::new(),
induction_vars: HashMap::new(),
memory_deps: HashMap::new(),
trip_counts: HashMap::new(),
iter_bounds: HashMap::new(),
carried_deps: HashMap::new(),
dominators: HashMap::new(),
post_dominators: HashMap::new(),
cfg: HashMap::new(),
reverse_cfg: HashMap::new(),
}
}
pub fn analyze(&mut self, mf: &MachineFunction) -> &[NaturalLoop] {
self.loops.clear();
self.induction_vars.clear();
self.memory_deps.clear();
self.trip_counts.clear();
self.iter_bounds.clear();
self.carried_deps.clear();
self.dominators.clear();
self.post_dominators.clear();
self.cfg.clear();
self.reverse_cfg.clear();
self.build_cfg(mf);
self.compute_all_dominators(mf);
self.loops = self.find_natural_loops(mf);
self.compute_loop_nesting();
let loops_snapshot = self.loops.clone();
for loop_info in &loops_snapshot {
let header = loop_info.header;
self.analyze_loop(loop_info, mf);
}
&self.loops
}
fn build_cfg(&mut self, mf: &MachineFunction) {
self.cfg.clear();
self.reverse_cfg.clear();
let mut name_to_idx: HashMap<&str, usize> = HashMap::new();
for (i, block) in mf.blocks.iter().enumerate() {
name_to_idx.insert(&block.name, i);
}
for (i, block) in mf.blocks.iter().enumerate() {
let succs: Vec<usize> = block
.successors
.iter()
.filter_map(|s| name_to_idx.get(s.as_str()).copied())
.collect();
for &s in &succs {
self.reverse_cfg.entry(s).or_default().push(i);
}
self.cfg.insert(i, succs);
}
}
fn compute_all_dominators(&mut self, mf: &MachineFunction) {
let n = mf.blocks.len();
if n == 0 {
return;
}
let all: BTreeSet<usize> = (0..n).collect();
let mut dom: Vec<BTreeSet<usize>> = Vec::with_capacity(n);
let mut entry_set = BTreeSet::new();
entry_set.insert(0);
dom.push(entry_set);
for _ in 1..n {
dom.push(all.clone());
}
let mut changed = true;
while changed {
changed = false;
for b in 0..n {
if b == 0 {
continue;
}
let preds = self.reverse_cfg.get(&b).cloned().unwrap_or_default();
let new_dom: BTreeSet<usize> = if preds.is_empty() {
BTreeSet::new()
} else {
let mut d = dom[preds[0]].clone();
for &p in &preds[1..] {
d = d.intersection(&dom[p]).copied().collect();
}
d
};
let mut final_dom = new_dom;
final_dom.insert(b);
if final_dom != dom[b] {
dom[b] = final_dom;
changed = true;
}
}
}
for (i, d) in dom.into_iter().enumerate() {
self.dominators.insert(i, d);
}
let mut pdom: Vec<BTreeSet<usize>> = Vec::with_capacity(n);
for _ in 0..n - 1 {
pdom.push(all.clone());
}
let mut last_set = BTreeSet::new();
last_set.insert(n - 1);
pdom.push(last_set);
let mut changed = true;
while changed {
changed = false;
for b in 0..n {
if b == n - 1 {
continue;
}
let succs = self.cfg.get(&b).cloned().unwrap_or_default();
let new_pdom: BTreeSet<usize> = if succs.is_empty() {
BTreeSet::new()
} else {
let mut d = pdom[succs[0]].clone();
for &s in &succs[1..] {
d = d.intersection(&pdom[s]).copied().collect();
}
d
};
let mut final_pdom = new_pdom;
final_pdom.insert(b);
if final_pdom != pdom[b] {
pdom[b] = final_pdom;
changed = true;
}
}
}
for (i, pd) in pdom.into_iter().enumerate() {
self.post_dominators.insert(i, pd);
}
}
fn find_natural_loops(&self, mf: &MachineFunction) -> Vec<NaturalLoop> {
let n = mf.blocks.len();
let mut loops = Vec::new();
let mut back_edges: Vec<(usize, usize)> = Vec::new();
for (src, succs) in &self.cfg {
for &dst in succs {
if self.dominates(dst, *src) {
back_edges.push((*src, dst));
}
}
}
for &(src, header) in &back_edges {
let mut loop_blocks = BTreeSet::new();
loop_blocks.insert(header);
let mut stack = vec![src];
if src != header {
loop_blocks.insert(src);
}
while let Some(node) = stack.pop() {
for &pred in self.reverse_cfg.get(&node).unwrap_or(&vec![]) {
if !loop_blocks.contains(&pred) {
loop_blocks.insert(pred);
stack.push(pred);
}
}
}
let mut latch_blocks = BTreeSet::new();
let mut exit_blocks = BTreeSet::new();
for &b in &loop_blocks {
if let Some(succs) = self.cfg.get(&b) {
if succs.contains(&header) {
latch_blocks.insert(b);
}
for s in succs {
if !loop_blocks.contains(s) {
exit_blocks.insert(b);
}
}
}
}
let preheader = (0..n)
.filter(|p| {
self.cfg.get(p).map_or(false, |s| s.contains(&header))
&& !loop_blocks.contains(p)
})
.next();
loops.push(NaturalLoop {
header,
blocks: loop_blocks,
back_edges: vec![(src, header)],
preheader,
exit_blocks,
latch_blocks,
depth: 0,
nesting_level: 0,
parent_loop: None,
child_loops: Vec::new(),
});
}
Self::merge_loops_by_header(loops)
}
fn dominates(&self, a: usize, b: usize) -> bool {
self.dominators
.get(&b)
.map(|d| d.contains(&a))
.unwrap_or(false)
}
fn merge_loops_by_header(loops: Vec<NaturalLoop>) -> Vec<NaturalLoop> {
let mut merged: BTreeMap<usize, NaturalLoop> = BTreeMap::new();
for l in loops {
let entry = merged.entry(l.header).or_insert_with(|| NaturalLoop {
header: l.header,
blocks: BTreeSet::new(),
back_edges: Vec::new(),
preheader: l.preheader,
exit_blocks: BTreeSet::new(),
latch_blocks: BTreeSet::new(),
depth: 0,
nesting_level: 0,
parent_loop: None,
child_loops: Vec::new(),
});
entry.blocks.extend(&l.blocks);
entry.back_edges.extend(&l.back_edges);
entry.exit_blocks.extend(&l.exit_blocks);
entry.latch_blocks.extend(&l.latch_blocks);
if entry.preheader.is_none() {
entry.preheader = l.preheader;
}
}
merged.into_values().collect()
}
fn compute_loop_nesting(&mut self) {
let n_loops = self.loops.len();
for i in 0..n_loops {
for j in 0..n_loops {
if i == j {
continue;
}
let outer = &self.loops[i];
let inner = &self.loops[j];
if inner.header != outer.header && outer.blocks.contains(&inner.header) {
let fully_contained = inner.blocks.iter().all(|b| outer.blocks.contains(b));
if fully_contained {
self.loops[j].parent_loop = Some(outer.header);
self.loops[j].nesting_level =
self.loops[j].nesting_level.max(outer.nesting_level + 1);
self.loops[i].child_loops.push(inner.header);
self.loops[j].depth = self.loops[j].nesting_level;
self.loops[i].depth = self.loops[i].nesting_level;
}
}
}
}
}
fn analyze_loop(&mut self, loop_info: &NaturalLoop, mf: &MachineFunction) {
let header = loop_info.header;
let trip_count = self.estimate_trip_count(loop_info, mf);
self.trip_counts.insert(header, trip_count);
let ind_vars = self.find_induction_variables(loop_info, mf);
self.induction_vars.insert(header, ind_vars);
let mem_deps = self.analyze_memory_dependencies(loop_info, mf);
self.memory_deps.insert(header, mem_deps);
let carried = self.find_loop_carried_dependencies(loop_info, mf);
self.carried_deps.insert(header, carried);
let bounds = self.compute_iteration_bounds(loop_info, mf);
self.iter_bounds.insert(header, bounds);
}
fn estimate_trip_count(&self, loop_info: &NaturalLoop, mf: &MachineFunction) -> u32 {
for &latch in &loop_info.latch_blocks {
if let Some(block) = mf.blocks.get(latch) {
for instr in &block.instructions {
if is_compare_opcode(instr.opcode) {
for op in &instr.operands {
if let MachineOperand::Imm(val) = op {
if *val > 0 {
return *val as u32;
}
}
}
}
}
}
}
if let Some(block) = mf.blocks.get(loop_info.header) {
for instr in &block.instructions {
if is_compare_opcode(instr.opcode) {
for op in &instr.operands {
if let MachineOperand::Imm(val) = op {
if *val > 0 {
return *val as u32;
}
}
}
}
}
}
if loop_info.nesting_level > 0 {
30
} else {
100
}
}
fn find_induction_variables(
&self,
loop_info: &NaturalLoop,
mf: &MachineFunction,
) -> Vec<InductionVariable> {
let mut ind_vars = Vec::new();
for &block_idx in &loop_info.blocks {
if let Some(block) = mf.blocks.get(block_idx) {
for (i, instr) in block.instructions.iter().enumerate() {
let opcode = instr.opcode;
if opcode == 2 || opcode == 3 {
if let Some(def) = instr.def {
let mut step: i64 = 0;
let mut has_imm = false;
let mut uses_self = false;
for op in &instr.operands {
if let MachineOperand::Imm(val) = op {
step = if opcode == 2 { *val } else { -*val };
has_imm = true;
}
if let MachineOperand::Reg(r) = op {
if *r == def {
uses_self = true;
}
}
}
if has_imm && step != 0 {
ind_vars.push(InductionVariable {
reg: def,
base: 0,
step,
is_primary: ind_vars.is_empty(),
is_derived: false,
derived_from: None,
end_reg: None,
counts_up: step > 0,
is_exit_condition: self.is_exit_condition(def, loop_info, mf),
def_instr_idx: i,
def_block_idx: block_idx,
});
}
}
}
}
}
}
if let Some(primary) = ind_vars.iter().find(|iv| iv.is_primary).cloned() {
for iv in &mut ind_vars {
if iv.reg != primary.reg && iv.step % primary.step == 0 {
iv.is_derived = true;
iv.derived_from = Some(primary.reg);
}
}
}
ind_vars
}
fn is_exit_condition(&self, reg: u32, loop_info: &NaturalLoop, mf: &MachineFunction) -> bool {
for &exit_block in &loop_info.exit_blocks {
if let Some(block) = mf.blocks.get(exit_block) {
for instr in &block.instructions {
if is_compare_opcode(instr.opcode) {
for op in &instr.operands {
if let MachineOperand::Reg(r) = op {
if *r == reg {
return true;
}
}
}
}
}
}
}
false
}
fn analyze_memory_dependencies(
&self,
loop_info: &NaturalLoop,
mf: &MachineFunction,
) -> Vec<MemDep> {
let mut deps = Vec::new();
let mut mem_instrs: Vec<(usize, usize, &MachineInstr)> = Vec::new();
for &block_idx in &loop_info.blocks {
if let Some(block) = mf.blocks.get(block_idx) {
for (i, instr) in block.instructions.iter().enumerate() {
if is_memory_opcode(instr.opcode) {
mem_instrs.push((block_idx, i, instr));
}
}
}
}
for a_idx in 0..mem_instrs.len() {
for b_idx in (a_idx + 1)..mem_instrs.len() {
let a = &mem_instrs[a_idx];
let b = &mem_instrs[b_idx];
let may_alias = true; let must_alias = self.same_memory_location(a.2, b.2);
let same_base = self.same_base_address(a.2, b.2);
deps.push(MemDep {
from: a_idx,
to: b_idx,
must_alias,
may_alias,
same_base,
distance: 0,
from_block: a.0,
to_block: b.0,
from_is_load: is_load_opcode(a.2.opcode),
to_is_load: is_load_opcode(b.2.opcode),
});
}
}
deps
}
fn same_memory_location(&self, a: &MachineInstr, b: &MachineInstr) -> bool {
if a.operands.len() < 2 || b.operands.len() < 2 {
return false;
}
a.operands[0] == b.operands[0] && a.operands[1] == b.operands[1]
}
fn same_base_address(&self, a: &MachineInstr, b: &MachineInstr) -> bool {
if a.operands.is_empty() || b.operands.is_empty() {
return false;
}
a.operands[0] == b.operands[0]
}
fn find_loop_carried_dependencies(
&self,
loop_info: &NaturalLoop,
mf: &MachineFunction,
) -> Vec<LoopCarriedDep> {
let mut carried = Vec::new();
let mut defs: HashMap<u32, (usize, usize)> = HashMap::new();
let mut uses_before_def: Vec<(u32, usize, usize)> = Vec::new();
for &block_idx in &loop_info.blocks {
if let Some(block) = mf.blocks.get(block_idx) {
for (i, instr) in block.instructions.iter().enumerate() {
for op in &instr.operands {
if let MachineOperand::Reg(r) = op {
if !defs.contains_key(r) {
uses_before_def.push((*r, block_idx, i));
}
}
}
if let Some(def) = instr.def {
defs.insert(def, (block_idx, i));
}
}
}
}
for &(reg, use_block, use_idx) in &uses_before_def {
if let Some(&(def_block, def_idx)) = defs.get(®) {
carried.push(LoopCarriedDep {
from: def_idx,
to: use_idx,
distance: 1,
latency: 1,
dep_type: DepType::RAW,
is_recurrence: true,
});
}
}
let mut last_store: Option<(usize, usize)> = None;
for &block_idx in &loop_info.blocks {
if let Some(block) = mf.blocks.get(block_idx) {
for (i, instr) in block.instructions.iter().enumerate() {
if is_store_opcode(instr.opcode) {
last_store = Some((block_idx, i));
} else if is_load_opcode(instr.opcode) {
if let Some((sblock, si)) = last_store {
if sblock <= block_idx {
carried.push(LoopCarriedDep {
from: si,
to: i,
distance: 1,
latency: 1,
dep_type: DepType::RAW,
is_recurrence: true,
});
}
}
}
}
}
}
carried
}
fn compute_iteration_bounds(
&self,
loop_info: &NaturalLoop,
mf: &MachineFunction,
) -> (i64, i64) {
let trip_count = self
.trip_counts
.get(&loop_info.header)
.copied()
.unwrap_or(10);
(0, trip_count as i64 * 2)
}
pub fn get_trip_count(&self, header: usize) -> u32 {
self.trip_counts.get(&header).copied().unwrap_or(0)
}
pub fn get_induction_variables(&self, header: usize) -> &[InductionVariable] {
self.induction_vars
.get(&header)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn has_carried_deps(&self, header: usize) -> bool {
self.carried_deps
.get(&header)
.map(|d| !d.is_empty())
.unwrap_or(false)
}
pub fn get_kernel_iterations(&self, header: usize, num_stages: u32) -> u32 {
let trip_count = self.get_trip_count(header);
if trip_count > num_stages {
trip_count - num_stages + 1
} else {
1
}
}
pub fn is_innermost(&self, loop_idx: usize) -> bool {
self.loops
.get(loop_idx)
.map(|l| l.child_loops.is_empty())
.unwrap_or(false)
}
pub fn get_innermost_first(&self) -> Vec<&NaturalLoop> {
let mut sorted: Vec<&NaturalLoop> = self.loops.iter().collect();
sorted.sort_by_key(|l| std::cmp::Reverse(l.nesting_level));
sorted
}
pub fn print_summary(&self) -> String {
let mut out = String::from("X86LoopAnalysis Summary:\n");
out.push_str(&format!(" Total loops found: {}\n", self.loops.len()));
for (i, loop_info) in self.loops.iter().enumerate() {
out.push_str(&format!(
" Loop {}: header={}, blocks={}, depth={}, nesting={}, trip_count={:?}\n",
i,
loop_info.header,
loop_info.blocks.len(),
loop_info.depth,
loop_info.nesting_level,
self.trip_counts.get(&loop_info.header),
));
}
out
}
}
impl Default for X86LoopAnalysis {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DepKind {
Flow,
Anti,
Output,
MemoryFlow,
MemoryAnti,
MemoryOutput,
Control,
}
#[derive(Debug, Clone)]
pub struct DepEdge {
pub from: usize,
pub to: usize,
pub kind: DepKind,
pub latency: u32,
pub distance: i32,
pub direction: i32,
pub register: Option<u32>,
pub is_carried: bool,
pub is_critical: bool,
pub min_latency: u32,
}
#[derive(Debug, Clone)]
pub struct DepNode {
pub instr_idx: usize,
pub block_idx: usize,
pub opcode: u32,
pub asap: i32,
pub alap: i32,
pub scheduled_cycle: i32,
pub slack: i32,
pub height: i32,
pub depth: i32,
pub successors: Vec<usize>,
pub predecessors: Vec<usize>,
pub scheduled: bool,
pub stage: i32,
pub resource_mask: u64,
pub latency: u32,
pub in_recurrence: bool,
pub uop_count: u32,
pub is_load: bool,
pub is_store: bool,
pub is_branch: bool,
pub defines_reg: Option<u32>,
pub uses_regs: Vec<u32>,
}
#[derive(Debug, Clone)]
pub struct X86DependenceGraph {
pub nodes: Vec<DepNode>,
pub edges: Vec<DepEdge>,
pub adj_out: Vec<Vec<usize>>,
pub adj_in: Vec<Vec<usize>>,
pub sccs: Vec<Vec<usize>>,
pub hooks: X86PipelinerHooks,
pub num_instrs: usize,
pub max_height: i32,
pub critical_path_edges: Vec<usize>,
}
impl X86DependenceGraph {
pub fn new(hooks: X86PipelinerHooks) -> Self {
X86DependenceGraph {
nodes: Vec::new(),
edges: Vec::new(),
adj_out: Vec::new(),
adj_in: Vec::new(),
sccs: Vec::new(),
hooks,
num_instrs: 0,
max_height: 0,
critical_path_edges: Vec::new(),
}
}
pub fn build(&mut self, loop_blocks: &[usize], mf: &MachineFunction) {
self.clear();
for &block_idx in loop_blocks {
if let Some(block) = mf.blocks.get(block_idx) {
for (i, instr) in block.instructions.iter().enumerate() {
let latency = self.hooks.get_latency(instr.opcode);
let resource_mask = self.hooks.get_resource_mask(instr.opcode);
let uop_count = self.hooks.get_uop_count(instr.opcode);
let uses: Vec<u32> = instr
.operands
.iter()
.filter_map(|op| match op {
MachineOperand::Reg(r) => Some(*r),
_ => None,
})
.collect();
self.nodes.push(DepNode {
instr_idx: i,
block_idx,
opcode: instr.opcode,
asap: -1,
alap: -1,
scheduled_cycle: -1,
slack: 0,
height: 0,
depth: 0,
successors: Vec::new(),
predecessors: Vec::new(),
scheduled: false,
stage: 0,
resource_mask,
latency,
in_recurrence: false,
uop_count,
is_load: is_load_opcode(instr.opcode),
is_store: is_store_opcode(instr.opcode),
is_branch: is_branch_opcode(instr.opcode),
defines_reg: instr.def,
uses_regs: uses,
});
self.adj_out.push(Vec::new());
self.adj_in.push(Vec::new());
}
}
}
self.num_instrs = self.nodes.len();
self.build_data_deps();
self.build_memory_deps();
self.build_control_deps();
self.find_sccs();
}
fn clear(&mut self) {
self.nodes.clear();
self.edges.clear();
self.adj_out.clear();
self.adj_in.clear();
self.sccs.clear();
self.num_instrs = 0;
self.max_height = 0;
self.critical_path_edges.clear();
}
fn build_data_deps(&mut self) {
let mut last_def: HashMap<u32, usize> = HashMap::new();
let mut last_use: HashMap<u32, Vec<usize>> = HashMap::new();
for node_idx in 0..self.nodes.len() {
let node = &self.nodes[node_idx];
for &r in &node.uses_regs {
if let Some(&def_node) = last_def.get(&r) {
self.add_edge(def_node, node_idx, DepKind::Flow, Some(r), 0);
}
last_use.entry(r).or_default().push(node_idx);
}
if let Some(def) = node.defines_reg {
if let Some(&prev_def) = last_def.get(&def) {
self.add_edge(prev_def, node_idx, DepKind::Output, Some(def), 0);
}
if let Some(uses) = last_use.get(&def) {
for &use_node in uses {
if use_node > last_def.get(&def).copied().unwrap_or(0) {
self.add_edge(use_node, node_idx, DepKind::Anti, Some(def), 0);
}
}
}
last_def.insert(def, node_idx);
last_use.remove(&def);
}
}
}
fn build_memory_deps(&mut self) {
let mut last_store: Option<usize> = None;
let mut loads_since_store: Vec<usize> = Vec::new();
let mut store_chain: Vec<usize> = Vec::new();
for node_idx in 0..self.nodes.len() {
let node = &self.nodes[node_idx];
if node.is_store {
for &load_node in &loads_since_store {
self.add_edge(load_node, node_idx, DepKind::MemoryAnti, None, 0);
}
for &prev_store in &store_chain {
self.add_edge(prev_store, node_idx, DepKind::MemoryOutput, None, 0);
}
last_store = Some(node_idx);
store_chain.push(node_idx);
loads_since_store.clear();
} else if node.is_load {
if let Some(&prev_store) = last_store.as_ref() {
self.add_edge(prev_store, node_idx, DepKind::MemoryFlow, None, 0);
}
loads_since_store.push(node_idx);
}
}
}
fn build_control_deps(&mut self) {
for node_idx in 0..self.nodes.len() {
let node = &self.nodes[node_idx];
if node.is_branch {
for other_idx in (node_idx + 1)..self.nodes.len() {
let other = &self.nodes[other_idx];
if other.block_idx == node.block_idx {
self.add_edge(node_idx, other_idx, DepKind::Control, None, 0);
}
}
}
}
}
fn add_edge(&mut self, from: usize, to: usize, kind: DepKind, reg: Option<u32>, distance: i32) {
if from >= self.nodes.len() || to >= self.nodes.len() {
return;
}
let latency = self.nodes[from].latency;
let edge_idx = self.edges.len();
self.edges.push(DepEdge {
from,
to,
kind,
latency,
distance,
direction: if distance > 0 { 1 } else { 0 },
register: reg,
is_carried: distance > 0,
is_critical: false,
min_latency: latency,
});
self.adj_out[from].push(edge_idx);
self.adj_in[to].push(edge_idx);
self.nodes[from].successors.push(to);
self.nodes[to].predecessors.push(from);
}
pub fn find_sccs(&mut self) {
#[derive(Clone, Copy)]
struct TarjanState {
index: i32,
lowlink: i32,
on_stack: bool,
}
let n = self.nodes.len();
let mut state = vec![
TarjanState {
index: -1,
lowlink: -1,
on_stack: false,
};
n
];
let mut stack: Vec<usize> = Vec::new();
let mut index_counter: i32 = 0;
self.sccs.clear();
for v in 0..n {
if state[v].index == -1 {
strongconnect(
v,
self,
&mut state,
&mut stack,
&mut index_counter,
&mut self.sccs,
);
}
}
for scc in &self.sccs {
if scc.len() > 1 {
for &node in scc {
self.nodes[node].in_recurrence = true;
}
}
}
}
pub fn compute_asap(&mut self) {
let n = self.nodes.len();
let mut indegree = vec![0usize; n];
for edge in &self.edges {
indegree[edge.to] += 1;
}
let mut queue: VecDeque<usize> = VecDeque::new();
for (i, °) in indegree.iter().enumerate() {
if deg == 0 {
self.nodes[i].asap = 0;
queue.push_back(i);
}
}
while let Some(u) = queue.pop_front() {
let u_asap = self.nodes[u].asap;
for &edge_idx in &self.adj_out[u] {
let edge = &self.edges[edge_idx];
let new_time = u_asap + edge.latency as i32;
let v = edge.to;
if new_time > self.nodes[v].asap {
self.nodes[v].asap = new_time;
}
indegree[v] -= 1;
if indegree[v] == 0 {
queue.push_back(v);
}
}
}
}
pub fn compute_alap(&mut self, max_cycle: i32) {
let n = self.nodes.len();
for node in &mut self.nodes {
node.alap = max_cycle;
}
let mut outdegree = vec![0usize; n];
for edge in &self.edges {
outdegree[edge.from] += 1;
}
let mut queue: VecDeque<usize> = VecDeque::new();
for (i, °) in outdegree.iter().enumerate() {
if deg == 0 {
queue.push_back(i);
}
}
while let Some(u) = queue.pop_front() {
let u_alap = self.nodes[u].alap;
for &edge_idx in &self.adj_in[u] {
let edge = &self.edges[edge_idx];
let new_time = u_alap - edge.latency as i32;
let v = edge.from;
if new_time < self.nodes[v].alap {
self.nodes[v].alap = new_time;
}
outdegree[v] -= 1;
if outdegree[v] == 0 {
queue.push_back(v);
}
}
}
}
pub fn compute_heights(&mut self) {
let n = self.nodes.len();
let mut heights = vec![0i32; n];
let mut changed = true;
while changed {
changed = false;
for u in 0..n {
let mut max_h: i32 = 0;
for &edge_idx in &self.adj_out[u] {
let edge = &self.edges[edge_idx];
let h = heights[edge.to] + edge.latency as i32;
max_h = max_h.max(h);
}
if max_h > heights[u] {
heights[u] = max_h;
changed = true;
}
}
}
self.max_height = *heights.iter().max().unwrap_or(&0);
for (i, node) in self.nodes.iter_mut().enumerate() {
node.height = heights[i];
}
}
pub fn compute_critical_path(&mut self) {
self.critical_path_edges.clear();
let n = self.nodes.len();
for (ei, edge) in self.edges.iter().enumerate() {
let from = edge.from;
let to = edge.to;
let path_through = self.nodes[from].depth + edge.latency as i32 + self.nodes[to].height;
if path_through == self.max_height {
self.critical_path_edges.push(ei);
}
}
}
pub fn critical_path_length(&self) -> i32 {
self.max_height
}
pub fn get_recurrences(&self) -> &[Vec<usize>] {
&self.sccs
}
pub fn compute_rec_mii(&self) -> u32 {
let mut rec_mii = 0u32;
for scc in &self.sccs {
if scc.len() <= 1 {
continue;
}
let mut total_latency: u32 = 0;
let mut total_distance: i32 = 0;
for &node_idx in scc {
for &edge_idx in &self.adj_out[node_idx] {
let edge = &self.edges[edge_idx];
if scc.contains(&edge.to) && edge.is_carried {
total_latency += edge.latency;
total_distance += edge.distance;
}
}
}
if total_distance > 0 {
let scc_mii = (total_latency as f64 / total_distance.abs() as f64).ceil() as u32;
rec_mii = rec_mii.max(scc_mii);
}
}
rec_mii.max(1)
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn edge_count(&self) -> usize {
self.edges.len()
}
}
impl Default for X86DependenceGraph {
fn default() -> Self {
Self::new(X86PipelinerHooks::default())
}
}
fn strongconnect(
v: usize,
graph: &X86DependenceGraph,
state: &mut [TarjanState],
stack: &mut Vec<usize>,
index_counter: &mut i32,
sccs: &mut Vec<Vec<usize>>,
) {
state[v].index = *index_counter;
state[v].lowlink = *index_counter;
*index_counter += 1;
stack.push(v);
state[v].on_stack = true;
for &edge_idx in &graph.adj_out[v] {
let w = graph.edges[edge_idx].to;
if state[w].index == -1 {
strongconnect(w, graph, state, stack, index_counter, sccs);
state[v].lowlink = state[v].lowlink.min(state[w].lowlink);
} else if state[w].on_stack {
state[v].lowlink = state[v].lowlink.min(state[w].index);
}
}
if state[v].lowlink == state[v].index {
let mut scc = Vec::new();
loop {
let w = stack.pop().unwrap();
state[w].on_stack = false;
scc.push(w);
if w == v {
break;
}
}
if scc.len() > 1 {
sccs.push(scc);
}
}
}
#[derive(Debug, Clone)]
pub struct SchedDAGNode {
pub dep_node_idx: usize,
pub opcode: u32,
pub latency: u32,
pub resource_mask: u64,
pub uop_count: u32,
pub asap: i32,
pub alap: i32,
pub scheduled_cycle: i32,
pub height: i32,
pub depth: i32,
pub is_critical: bool,
pub in_recurrence: bool,
}
#[derive(Debug, Clone)]
pub struct SchedDAGEdge {
pub from: usize,
pub to: usize,
pub latency: u32,
pub distance: i32,
pub kind: DepKind,
}
#[derive(Debug, Clone)]
pub struct X86ScheduleDAG {
pub nodes: Vec<SchedDAGNode>,
pub edges: Vec<SchedDAGEdge>,
pub successors: Vec<Vec<usize>>,
pub predecessors: Vec<Vec<usize>>,
pub topo_order: Vec<usize>,
pub critical_path_len: u32,
pub hooks: X86PipelinerHooks,
}
impl X86ScheduleDAG {
pub fn new(hooks: X86PipelinerHooks) -> Self {
X86ScheduleDAG {
nodes: Vec::new(),
edges: Vec::new(),
successors: Vec::new(),
predecessors: Vec::new(),
topo_order: Vec::new(),
critical_path_len: 0,
hooks,
}
}
pub fn build_from_dep_graph(&mut self, dep_graph: &X86DependenceGraph) {
self.clear();
let n = dep_graph.node_count();
for (i, dep_node) in dep_graph.nodes.iter().enumerate() {
self.nodes.push(SchedDAGNode {
dep_node_idx: i,
opcode: dep_node.opcode,
latency: dep_node.latency,
resource_mask: dep_node.resource_mask,
uop_count: dep_node.uop_count,
asap: dep_node.asap,
alap: dep_node.alap,
scheduled_cycle: -1,
height: dep_node.height,
depth: dep_node.depth,
is_critical: false,
in_recurrence: dep_node.in_recurrence,
});
self.successors.push(Vec::new());
self.predecessors.push(Vec::new());
}
for edge in &dep_graph.edges {
let ei = self.edges.len();
self.edges.push(SchedDAGEdge {
from: edge.from,
to: edge.to,
latency: edge.latency,
distance: edge.distance,
kind: edge.kind,
});
self.successors[edge.from].push(ei);
self.predecessors[edge.to].push(ei);
}
self.compute_topo_order();
self.compute_critical_path();
}
fn clear(&mut self) {
self.nodes.clear();
self.edges.clear();
self.successors.clear();
self.predecessors.clear();
self.topo_order.clear();
self.critical_path_len = 0;
}
fn compute_topo_order(&mut self) {
let n = self.nodes.len();
let mut indegree = vec![0; n];
for edge in &self.edges {
indegree[edge.to] += 1;
}
let mut queue: VecDeque<usize> = VecDeque::new();
for (i, °) in indegree.iter().enumerate() {
if deg == 0 {
queue.push_back(i);
}
}
self.topo_order.clear();
while let Some(u) = queue.pop_front() {
self.topo_order.push(u);
for &ei in &self.successors[u] {
let v = self.edges[ei].to;
indegree[v] -= 1;
if indegree[v] == 0 {
queue.push_back(v);
}
}
}
}
fn compute_critical_path(&mut self) {
let n = self.nodes.len();
let mut dist = vec![0u32; n];
for &u in &self.topo_order {
let d = dist[u];
for &ei in &self.successors[u] {
let edge = &self.edges[ei];
let v = edge.to;
let nd = d + edge.latency;
if nd > dist[v] {
dist[v] = nd;
}
}
}
self.critical_path_len = *dist.iter().max().unwrap_or(&0);
let mut on_critical = vec![false; n];
let mut rev_topo = self.topo_order.clone();
rev_topo.reverse();
for &u in &rev_topo {
if dist[u] == self.critical_path_len {
on_critical[u] = true;
}
if on_critical[u] {
for &ei in &self.predecessors[u] {
let edge = &self.edges[ei];
let p = edge.from;
if dist[p] + edge.latency == dist[u] {
on_critical[p] = true;
}
}
}
}
for (i, node) in self.nodes.iter_mut().enumerate() {
node.is_critical = on_critical[i];
}
}
pub fn priority_order(&self) -> Vec<usize> {
let mut indices: Vec<usize> = (0..self.nodes.len()).collect();
indices.sort_by(|&a, &b| {
self.nodes[b]
.height
.cmp(&self.nodes[a].height)
.then_with(|| self.nodes[b].depth.cmp(&self.nodes[a].depth))
.then_with(|| self.nodes[b].is_critical.cmp(&self.nodes[a].is_critical))
.then_with(|| {
self.nodes[b]
.in_recurrence
.cmp(&self.nodes[a].in_recurrence)
})
});
indices
}
pub fn modulo_order(&self) -> Vec<usize> {
self.priority_order()
}
pub fn resource_heights(&self) -> Vec<i32> {
self.nodes
.iter()
.map(|n| n.height + n.resource_mask.count_ones() as i32)
.collect()
}
pub fn get_successors(&self, node: usize) -> Vec<usize> {
self.successors[node]
.iter()
.map(|&e| self.edges[e].to)
.collect()
}
pub fn get_predecessors(&self, node: usize) -> Vec<usize> {
self.predecessors[node]
.iter()
.map(|&e| self.edges[e].from)
.collect()
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
}
impl Default for X86ScheduleDAG {
fn default() -> Self {
Self::new(X86PipelinerHooks::default())
}
}
#[derive(Debug, Clone)]
pub struct ModuloReservationTable {
pub ii: u32,
pub num_resources: usize,
pub table: Vec<Vec<u32>>,
pub max_usage: Vec<u32>,
}
impl ModuloReservationTable {
pub fn new(ii: u32, num_resources: usize, max_usage: &[u32]) -> Self {
let table = vec![vec![0u32; ii as usize]; num_resources];
ModuloReservationTable {
ii,
num_resources,
table,
max_usage: max_usage.to_vec(),
}
}
pub fn can_place(&self, resource_mask: u64, cycle: i32) -> bool {
let m = (cycle % self.ii as i32).unsigned_abs() as usize;
for r in 0..self.num_resources.min(64) {
if (resource_mask >> r) & 1 == 1 {
if r < self.table.len() && m < self.table[r].len() {
if self.table[r][m] >= self.max_usage[r] {
return false;
}
}
}
}
true
}
pub fn reserve(&mut self, resource_mask: u64, cycle: i32) {
let m = (cycle % self.ii as i32).unsigned_abs() as usize;
for r in 0..self.num_resources.min(64) {
if (resource_mask >> r) & 1 == 1 {
if r < self.table.len() && m < self.table[r].len() {
self.table[r][m] += 1;
}
}
}
}
pub fn release(&mut self, resource_mask: u64, cycle: i32) {
let m = (cycle % self.ii as i32).unsigned_abs() as usize;
for r in 0..self.num_resources.min(64) {
if (resource_mask >> r) & 1 == 1 {
if r < self.table.len() && m < self.table[r].len() && self.table[r][m] > 0 {
self.table[r][m] -= 1;
}
}
}
}
pub fn reset(&mut self) {
for row in &mut self.table {
row.fill(0);
}
}
pub fn utilization(&self, cycle: i32) -> f64 {
let m = (cycle % self.ii as i32).unsigned_abs() as usize;
let mut used = 0;
let mut total = 0;
for r in 0..self.num_resources {
total += self.max_usage[r] as usize;
if m < self.table.get(r).map(|row| row.len()).unwrap_or(0) {
used += self.table[r][m] as usize;
}
}
if total == 0 {
0.0
} else {
used as f64 / total as f64
}
}
}
#[derive(Debug, Clone)]
pub struct X86ModuloScheduler {
pub hooks: X86PipelinerHooks,
pub dag: X86ScheduleDAG,
pub dep_graph: X86DependenceGraph,
pub ii: u32,
pub max_ii: u32,
pub res_mii: u32,
pub rec_mii: u32,
pub min_ii: u32,
pub schedule: Vec<i32>,
pub stages: Vec<i32>,
pub succeeded: bool,
pub backtrack_count: u32,
pub max_backtrack: u32,
pub res_table: ModuloReservationTable,
}
impl X86ModuloScheduler {
pub fn new(
hooks: X86PipelinerHooks,
dag: X86ScheduleDAG,
dep_graph: X86DependenceGraph,
) -> Self {
let num_ports = hooks.num_ports as usize;
let resources = hooks.port_resources.clone();
X86ModuloScheduler {
hooks: hooks.clone(),
dag,
dep_graph,
ii: 1,
max_ii: DEFAULT_MAX_II,
res_mii: 0,
rec_mii: 0,
min_ii: 0,
schedule: Vec::new(),
stages: Vec::new(),
succeeded: false,
backtrack_count: 0,
max_backtrack: DEFAULT_MAX_BACKTRACK,
res_table: ModuloReservationTable::new(1, num_ports, &resources),
}
}
pub fn schedule(&mut self) -> bool {
self.res_mii = self.compute_res_mii();
self.rec_mii = self.dep_graph.compute_rec_mii();
self.min_ii = self.res_mii.max(self.rec_mii).max(1);
for ii in self.min_ii..=self.max_ii {
self.ii = ii;
if self.try_schedule_at_ii(ii) {
self.succeeded = true;
return true;
}
}
self.succeeded = false;
false
}
fn compute_res_mii(&self) -> u32 {
let n = self.dag.nodes.len();
if n == 0 {
return 1;
}
let mut total_usage = vec![0u64; self.hooks.num_ports as usize];
for node in &self.dag.nodes {
for r in 0..self.hooks.num_ports.min(64) as usize {
if (node.resource_mask >> r) & 1 == 1 {
total_usage[r] += 1;
}
}
}
let mut max_ratio = 0f64;
for r in 0..self.hooks.num_ports.min(64) as usize {
if r < self.hooks.port_resources.len() && self.hooks.port_resources[r] > 0 {
let ratio = total_usage[r] as f64 / self.hooks.port_resources[r] as f64;
max_ratio = max_ratio.max(ratio);
}
}
(max_ratio.ceil() as u32).max(1)
}
fn try_schedule_at_ii(&mut self, ii: u32) -> bool {
let n = self.dag.nodes.len();
self.schedule = vec![-1; n];
self.stages = vec![0; n];
self.backtrack_count = 0;
let num_ports = self.hooks.num_ports as usize;
let resources = self.hooks.port_resources.clone();
self.res_table = ModuloReservationTable::new(ii, num_ports, &resources);
self.compute_asap_alap(ii);
let order = self.dag.priority_order();
let mut pos = 0;
while pos < order.len() {
let node_idx = order[pos];
if let Some(cycle) = self.find_slot(node_idx, ii) {
self.place_node(node_idx, cycle, ii);
pos += 1;
} else {
self.backtrack_count += 1;
if self.backtrack_count > self.max_backtrack {
return false;
}
if pos > 0 {
pos -= 1;
let prev = order[pos];
self.remove_node(prev);
} else {
return false;
}
}
}
true
}
fn compute_asap_alap(&mut self, ii: u32) {
let n = self.dag.nodes.len();
let mut indegree = vec![0usize; n];
for edge in &self.dag.edges {
indegree[edge.to] += 1;
}
let mut queue: VecDeque<usize> = VecDeque::new();
for (i, °) in indegree.iter().enumerate() {
if deg == 0 {
self.dag.nodes[i].asap = 0;
queue.push_back(i);
}
}
while let Some(u) = queue.pop_front() {
let u_asap = self.dag.nodes[u].asap;
for &ei in &self.dag.successors[u] {
let edge = &self.dag.edges[ei];
let na = u_asap + edge.latency as i32 - edge.distance * ii as i32;
let v = edge.to;
if na > self.dag.nodes[v].asap {
self.dag.nodes[v].asap = na;
}
indegree[v] -= 1;
if indegree[v] == 0 {
queue.push_back(v);
}
}
}
let max_cycle = n as i32 * ii as i32;
for node in &mut self.dag.nodes {
node.alap = max_cycle;
}
let mut outdegree = vec![0usize; n];
for edge in &self.dag.edges {
outdegree[edge.from] += 1;
}
queue.clear();
for (i, °) in outdegree.iter().enumerate() {
if deg == 0 {
queue.push_back(i);
}
}
while let Some(u) = queue.pop_front() {
let u_alap = self.dag.nodes[u].alap;
for &ei in &self.dag.predecessors[u] {
let edge = &self.dag.edges[ei];
let na = u_alap - edge.latency as i32 + edge.distance * ii as i32;
let v = edge.from;
if na < self.dag.nodes[v].alap {
self.dag.nodes[v].alap = na;
}
outdegree[v] -= 1;
if outdegree[v] == 0 {
queue.push_back(v);
}
}
}
}
fn find_slot(&self, node_idx: usize, _ii: u32) -> Option<i32> {
let node = &self.dag.nodes[node_idx];
let early = node.asap;
let late = node.alap;
for cycle in early..=late {
if self.can_place_at(node_idx, cycle) {
return Some(cycle);
}
}
None
}
fn can_place_at(&self, node_idx: usize, cycle: i32) -> bool {
let node = &self.dag.nodes[node_idx];
if !self.res_table.can_place(node.resource_mask, cycle) {
return false;
}
for &ei in &self.dag.predecessors[node_idx] {
let edge = &self.dag.edges[ei];
let pc = self.schedule[edge.from];
if pc >= 0 && cycle < pc + edge.latency as i32 - edge.distance {
return false;
}
}
for &ei in &self.dag.successors[node_idx] {
let edge = &self.dag.edges[ei];
let sc = self.schedule[edge.to];
if sc >= 0 && sc < cycle + edge.latency as i32 - edge.distance {
return false;
}
}
true
}
fn place_node(&mut self, node_idx: usize, cycle: i32, ii: u32) {
let node = &self.dag.nodes[node_idx];
self.res_table.reserve(node.resource_mask, cycle);
self.schedule[node_idx] = cycle;
self.stages[node_idx] = cycle / ii as i32;
self.dag.nodes[node_idx].scheduled_cycle = cycle;
}
fn remove_node(&mut self, node_idx: usize) {
if self.schedule[node_idx] >= 0 {
let node = &self.dag.nodes[node_idx];
self.res_table
.release(node.resource_mask, self.schedule[node_idx]);
self.schedule[node_idx] = -1;
self.stages[node_idx] = 0;
self.dag.nodes[node_idx].scheduled_cycle = -1;
}
}
pub fn num_stages(&self) -> u32 {
self.stages.iter().max().copied().unwrap_or(0) as u32 + 1
}
pub fn get_schedule(&self) -> Vec<(usize, i32, i32)> {
(0..self.schedule.len())
.map(|i| (i, self.schedule[i], self.stages[i]))
.collect()
}
pub fn print_stats(&self) -> String {
format!(
"X86ModuloScheduler: II={}, ResMII={}, RecMII={}, nodes={}, stages={}, succeeded={}, backtracks={}",
self.ii,
self.res_mii,
self.rec_mii,
self.dag.nodes.len(),
self.num_stages(),
self.succeeded,
self.backtrack_count
)
}
}
impl Default for X86ModuloScheduler {
fn default() -> Self {
Self::new(
X86PipelinerHooks::default(),
X86ScheduleDAG::default(),
X86DependenceGraph::default(),
)
}
}
#[derive(Debug, Clone)]
pub struct KernelInstruction {
pub instr: MachineInstr,
pub stage: u32,
pub cycle: u32,
pub is_prologue: bool,
pub is_kernel: bool,
pub is_epilogue: bool,
pub predicate: Option<PredicateInfo>,
}
#[derive(Debug, Clone)]
pub struct PredicateInfo {
pub pred_reg: u32,
pub sense: bool,
pub trigger_stage: u32,
}
#[derive(Debug, Clone)]
pub struct X86KernelExpansion {
pub ii: u32,
pub num_stages: u32,
pub prologue: Vec<Vec<KernelInstruction>>,
pub kernel: Vec<KernelInstruction>,
pub epilogue: Vec<Vec<KernelInstruction>>,
pub original_trip_count: u32,
pub kernel_trip_count: u32,
pub hooks: X86PipelinerHooks,
pub use_predication: bool,
pub mve: Option<ModuloVariableExpansion>,
}
impl X86KernelExpansion {
pub fn new(ii: u32, num_stages: u32, trip_count: u32, hooks: X86PipelinerHooks) -> Self {
let ktc = if trip_count > num_stages {
trip_count - num_stages + 1
} else {
1
};
X86KernelExpansion {
ii,
num_stages,
prologue: Vec::new(),
kernel: Vec::new(),
epilogue: Vec::new(),
original_trip_count: trip_count,
kernel_trip_count: ktc,
hooks,
use_predication: false,
mve: None,
}
}
pub fn expand(&mut self, schedule: &[(usize, i32, i32)], loop_instrs: &[MachineInstr]) {
self.prologue.clear();
self.kernel.clear();
self.epilogue.clear();
let mut stage_instrs: BTreeMap<i32, Vec<&MachineInstr>> = BTreeMap::new();
for &(ni, _c, stage) in schedule {
if let Some(instr) = loop_instrs.get(ni) {
stage_instrs.entry(stage).or_default().push(instr);
}
}
let total_stages = stage_instrs.keys().max().copied().unwrap_or(0) + 1;
for k in 0..(total_stages - 1).max(1) {
let mut block = Vec::new();
for s in 0..=k {
if let Some(instrs) = stage_instrs.get(&s) {
for &instr in instrs {
block.push(KernelInstruction {
instr: instr.clone(),
stage: s as u32,
cycle: 0,
is_prologue: true,
is_kernel: false,
is_epilogue: false,
predicate: None,
});
}
}
}
if !block.is_empty() {
self.prologue.push(block);
}
}
for s in 0..total_stages {
if let Some(instrs) = stage_instrs.get(&s) {
for &instr in instrs {
self.kernel.push(KernelInstruction {
instr: instr.clone(),
stage: s as u32,
cycle: s as u32,
is_prologue: false,
is_kernel: true,
is_epilogue: false,
predicate: None,
});
}
}
}
self.kernel.sort_by_key(|k| k.stage);
for k in 1..total_stages {
let mut block = Vec::new();
for s in k..total_stages {
if let Some(instrs) = stage_instrs.get(&s) {
for &instr in instrs {
block.push(KernelInstruction {
instr: instr.clone(),
stage: s as u32,
cycle: 0,
is_prologue: false,
is_kernel: false,
is_epilogue: true,
predicate: None,
});
}
}
}
if !block.is_empty() {
self.epilogue.push(block);
}
}
self.num_stages = total_stages as u32;
self.kernel_trip_count = if self.original_trip_count > self.num_stages {
self.original_trip_count - self.num_stages + 1
} else {
1
};
}
pub fn generate_prologue_blocks(&self) -> Vec<MachineBasicBlock> {
self.prologue
.iter()
.enumerate()
.map(|(i, block)| MachineBasicBlock {
name: format!("prologue_{}", i),
instructions: block.iter().map(|k| k.instr.clone()).collect(),
successors: Vec::new(),
})
.collect()
}
pub fn generate_kernel_block(&self) -> MachineBasicBlock {
MachineBasicBlock {
name: "kernel".into(),
instructions: self.kernel.iter().map(|k| k.instr.clone()).collect(),
successors: Vec::new(),
}
}
pub fn generate_epilogue_blocks(&self) -> Vec<MachineBasicBlock> {
self.epilogue
.iter()
.enumerate()
.map(|(i, block)| MachineBasicBlock {
name: format!("epilogue_{}", i),
instructions: block.iter().map(|k| k.instr.clone()).collect(),
successors: Vec::new(),
})
.collect()
}
pub fn total_instructions(&self) -> usize {
self.prologue.iter().map(|b| b.len()).sum::<usize>()
+ self.kernel.len()
+ self.epilogue.iter().map(|b| b.len()).sum::<usize>()
}
pub fn code_expansion_factor(&self) -> f64 {
let orig = self.kernel.len() as f64;
if orig == 0.0 {
1.0
} else {
self.total_instructions() as f64 / orig
}
}
pub fn enable_predication(&mut self) {
self.use_predication = true;
}
pub fn set_mve(&mut self, mve: ModuloVariableExpansion) {
self.mve = Some(mve);
}
pub fn print_stats(&self) -> String {
format!(
"X86KernelExpansion: II={}, stages={}, trips={}, kernel_trips={}, \
prologue_blocks={}, kernel_instrs={}, epilogue_blocks={}, expansion={:.2}x",
self.ii,
self.num_stages,
self.original_trip_count,
self.kernel_trip_count,
self.prologue.len(),
self.kernel.len(),
self.epilogue.len(),
self.code_expansion_factor()
)
}
}
impl Default for X86KernelExpansion {
fn default() -> Self {
Self::new(1, 1, 10, X86PipelinerHooks::default())
}
}
#[derive(Debug, Clone)]
pub struct X86SoftwarePipelining {
pub hooks: X86PipelinerHooks,
pub loop_analysis: X86LoopAnalysis,
pub dep_graph: Option<X86DependenceGraph>,
pub sched_dag: Option<X86ScheduleDAG>,
pub scheduler: Option<X86ModuloScheduler>,
pub expansion: Option<X86KernelExpansion>,
pub pipelines_created: usize,
pub total_kernel_instructions: usize,
pub best_ii_achieved: u32,
pub enabled: bool,
pub pipelined_loops: Vec<usize>,
}
impl X86SoftwarePipelining {
pub fn new(hooks: X86PipelinerHooks) -> Self {
X86SoftwarePipelining {
hooks,
loop_analysis: X86LoopAnalysis::new(),
dep_graph: None,
sched_dag: None,
scheduler: None,
expansion: None,
pipelines_created: 0,
total_kernel_instructions: 0,
best_ii_achieved: u32::MAX,
enabled: true,
pipelined_loops: Vec::new(),
}
}
pub fn run_on_function(&mut self, mf: &mut MachineFunction) -> usize {
if !self.enabled {
return 0;
}
self.pipelines_created = 0;
self.total_kernel_instructions = 0;
self.pipelined_loops.clear();
let loops = self.loop_analysis.analyze(mf).to_vec();
for loop_info in &loops {
if !self.try_pipeline_loop(loop_info, mf) {
continue;
}
self.pipelined_loops.push(loop_info.header);
}
self.pipelines_created
}
fn try_pipeline_loop(&mut self, loop_info: &NaturalLoop, mf: &mut MachineFunction) -> bool {
let trip_count = self.loop_analysis.get_trip_count(loop_info.header);
let body_size: usize = loop_info
.blocks
.iter()
.map(|&b| {
mf.blocks
.get(b)
.map(|bb| bb.instructions.len())
.unwrap_or(0)
})
.sum();
let has_carried = self.loop_analysis.has_carried_deps(loop_info.header);
if !self
.hooks
.should_pipeline(trip_count, body_size, has_carried)
{
return false;
}
let loop_blocks: Vec<usize> = loop_info.blocks.iter().copied().collect();
if loop_blocks.is_empty() {
return false;
}
let mut dep_graph = X86DependenceGraph::new(self.hooks.clone());
dep_graph.build(&loop_blocks, mf);
dep_graph.compute_asap();
dep_graph.compute_alap(dep_graph.node_count() as i32 * 4);
dep_graph.compute_heights();
let mut sched_dag = X86ScheduleDAG::new(self.hooks.clone());
sched_dag.build_from_dep_graph(&dep_graph);
let mut scheduler =
X86ModuloScheduler::new(self.hooks.clone(), sched_dag.clone(), dep_graph.clone());
if !scheduler.schedule() {
return false;
}
let ii = scheduler.ii;
let num_stages = scheduler.num_stages();
let loop_instrs: Vec<MachineInstr> = dep_graph
.nodes
.iter()
.map(|n| {
mf.blocks
.get(n.block_idx)
.and_then(|b| b.instructions.get(n.instr_idx))
.cloned()
.unwrap_or_else(|| MachineInstr::new(0))
})
.collect();
let mut expansion = X86KernelExpansion::new(ii, num_stages, trip_count, self.hooks.clone());
expansion.expand(&scheduler.get_schedule(), &loop_instrs);
let prologue = expansion.generate_prologue_blocks();
let kernel = expansion.generate_kernel_block();
let epilogue = expansion.generate_epilogue_blocks();
if !Self::apply_pipeline(mf, loop_info, prologue, kernel, epilogue) {
return false;
}
self.pipelines_created += 1;
self.best_ii_achieved = self.best_ii_achieved.min(ii);
self.total_kernel_instructions += expansion.kernel.len();
self.dep_graph = Some(dep_graph);
self.sched_dag = Some(sched_dag);
self.scheduler = Some(scheduler);
self.expansion = Some(expansion);
true
}
fn apply_pipeline(
mf: &mut MachineFunction,
loop_info: &NaturalLoop,
prologue: Vec<MachineBasicBlock>,
kernel: MachineBasicBlock,
epilogue: Vec<MachineBasicBlock>,
) -> bool {
let insert_pos = loop_info
.preheader
.map(|p| p + 1)
.unwrap_or(loop_info.header);
let mut indices: Vec<usize> = loop_info.blocks.iter().copied().collect();
indices.sort_unstable_by(|a, b| b.cmp(a));
for &idx in &indices {
if idx < mf.blocks.len() {
mf.blocks.remove(idx);
}
}
let mut pos = insert_pos.min(mf.blocks.len());
for block in prologue {
mf.blocks.insert(pos, block);
pos += 1;
}
mf.blocks.insert(pos, kernel);
pos += 1;
for block in epilogue {
mf.blocks.insert(pos, block);
pos += 1;
}
true
}
pub fn print_summary(&self) -> String {
let mut out = String::from("X86SoftwarePipelining Summary:\n");
out.push_str(&format!(
" Pipelines created: {}\n",
self.pipelines_created
));
out.push_str(&format!(
" Best II: {}\n",
if self.best_ii_achieved == u32::MAX {
"N/A".to_string()
} else {
self.best_ii_achieved.to_string()
}
));
out.push_str(&format!(
" Total kernel instrs: {}\n",
self.total_kernel_instructions
));
if let Some(ref sched) = self.scheduler {
out.push_str(&format!(" {}\n", sched.print_stats()));
}
if let Some(ref exp) = self.expansion {
out.push_str(&format!(" {}\n", exp.print_stats()));
}
out
}
}
impl Default for X86SoftwarePipelining {
fn default() -> Self {
Self::new(X86PipelinerHooks::default())
}
}
#[derive(Debug, Clone)]
pub struct X86MachinePipeliner {
pub software_pipelining: X86SoftwarePipelining,
pub hooks: X86PipelinerHooks,
pub functions_processed: usize,
pub functions_pipelined: usize,
pub total_pipelines: usize,
pub total_kernel_instrs: usize,
pub enabled: bool,
pub max_expansion_factor: f64,
pub min_trip_count: u32,
}
impl X86MachinePipeliner {
pub fn new(sched_model: X86SchedModel) -> Self {
let hooks = X86PipelinerHooks::new(sched_model);
let swp = X86SoftwarePipelining::new(hooks.clone());
X86MachinePipeliner {
software_pipelining: swp,
hooks,
functions_processed: 0,
functions_pipelined: 0,
total_pipelines: 0,
total_kernel_instrs: 0,
enabled: true,
max_expansion_factor: MAX_CODE_EXPANSION_FACTOR,
min_trip_count: MIN_TRIP_COUNT_FOR_PIPELINING,
}
}
pub fn run_on_function(&mut self, mf: &mut MachineFunction) -> bool {
if !self.enabled {
return false;
}
self.functions_processed += 1;
let pipelines = self.software_pipelining.run_on_function(mf);
if pipelines > 0 {
self.functions_pipelined += 1;
self.total_pipelines += pipelines;
self.total_kernel_instrs += self.software_pipelining.total_kernel_instructions;
return true;
}
false
}
pub fn should_pipeline_loop(
&self,
trip_count: u32,
body_size: usize,
has_carried: bool,
) -> bool {
self.hooks
.should_pipeline(trip_count, body_size, has_carried)
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
self.software_pipelining.enabled = enabled;
}
pub fn set_max_expansion(&mut self, factor: f64) {
self.max_expansion_factor = factor;
}
pub fn reset_stats(&mut self) {
self.functions_processed = 0;
self.functions_pipelined = 0;
self.total_pipelines = 0;
self.total_kernel_instrs = 0;
self.software_pipelining.pipelines_created = 0;
self.software_pipelining.total_kernel_instructions = 0;
self.software_pipelining.best_ii_achieved = u32::MAX;
}
pub fn print_stats(&self) -> String {
let mut out = String::from("X86 Machine Pipeliner Stats:\n");
out.push_str(&format!(
" Functions processed: {}\n",
self.functions_processed
));
out.push_str(&format!(
" Functions pipelined: {}\n",
self.functions_pipelined
));
out.push_str(&format!(" Total pipelines: {}\n", self.total_pipelines));
out.push_str(&format!(
" Total kernel instrs: {}\n",
self.total_kernel_instrs
));
if self.total_pipelines > 0 {
out.push_str(&format!(
" Avg kernel instrs/pipeline: {:.1}\n",
self.total_kernel_instrs as f64 / self.total_pipelines as f64
));
}
out.push_str(&format!(" Model: {:?}\n", self.hooks.sched_model));
out.push_str(&self.software_pipelining.print_summary());
out
}
pub fn get_hooks(&self) -> X86PipelinerHooks {
self.hooks.clone()
}
}
impl Default for X86MachinePipeliner {
fn default() -> Self {
Self::new(X86SchedModel::Generic)
}
}
#[derive(Debug, Clone)]
pub struct X86ModuloVariableExpander {
pub mve: ModuloVariableExpansion,
pub hooks: X86PipelinerHooks,
pub stage_registers: HashMap<(u32, u32), u32>,
pub new_vregs: u32,
}
impl X86ModuloVariableExpander {
pub fn new(ii: u32, num_stages: u32, hooks: X86PipelinerHooks) -> Self {
X86ModuloVariableExpander {
mve: ModuloVariableExpansion::new(ii, num_stages, hooks.supports_rotating_regs),
hooks,
stage_registers: HashMap::new(),
new_vregs: 0,
}
}
pub fn expand_value(
&mut self,
original_vreg: u32,
def_stage: u32,
use_stages: &[u32],
) -> Vec<(u32, u32)> {
let mut assignments = Vec::new();
for &stage in use_stages {
let new_vreg = self.mve.next_vreg + self.new_vregs;
self.stage_registers
.insert((original_vreg, stage), new_vreg);
assignments.push((stage, new_vreg));
self.new_vregs += 1;
}
self.stage_registers
.entry((original_vreg, def_stage))
.or_insert_with(|| {
self.new_vregs += 1;
self.mve.next_vreg + self.new_vregs - 1
});
assignments
}
pub fn get_register_for_stage(&self, vreg: u32, stage: u32) -> Option<u32> {
self.stage_registers.get(&(vreg, stage)).copied()
}
pub fn rewrite_instructions(&self, instructions: &mut [KernelInstruction]) {
for ki in instructions {
let stage = ki.stage;
if let Some(def) = ki.instr.def {
if let Some(&nv) = self.stage_registers.get(&(def, stage)) {
ki.instr.def = Some(nv);
}
}
for op in &mut ki.instr.operands {
if let MachineOperand::Reg(ref mut r) = op {
if let Some(&nv) = self.stage_registers.get(&(*r, stage)) {
*r = nv;
}
}
}
}
}
pub fn generate_copies(&self) -> Vec<(u32, u32, u32)> {
let mut copies = Vec::new();
for ((vreg, stage), _) in &self.stage_registers {
if *stage > 0 {
if let Some(_prev) = self.stage_registers.get(&(*vreg, stage - 1)) {
copies.push((*vreg, stage - 1, *stage));
}
}
}
copies
}
pub fn is_expanded(&self, vreg: u32) -> bool {
self.stage_registers.keys().any(|(r, _)| *r == vreg)
}
pub fn print_stats(&self) -> String {
let unique_regs: BTreeSet<u32> = self.stage_registers.keys().map(|(r, _)| *r).collect();
format!(
"X86MVE: expanded_regs={}, new_vregs={}, copies={}",
unique_regs.len(),
self.new_vregs,
self.generate_copies().len()
)
}
}
impl Default for X86ModuloVariableExpander {
fn default() -> Self {
Self::new(1, 1, X86PipelinerHooks::default())
}
}
#[derive(Debug, Clone)]
pub struct X86IterativeModuloScheduler {
pub res_mii: u32,
pub rec_mii: u32,
pub current_ii: u32,
pub max_ii: u32,
pub schedule: Vec<i32>,
pub succeeded: bool,
pub attempts: u32,
}
impl X86IterativeModuloScheduler {
pub fn new(res_mii: u32, rec_mii: u32, max_ii: u32) -> Self {
X86IterativeModuloScheduler {
res_mii,
rec_mii,
current_ii: res_mii.max(rec_mii).max(1),
max_ii,
schedule: Vec::new(),
succeeded: false,
attempts: 0,
}
}
pub fn run(&mut self, dag: &X86ScheduleDAG, hooks: &X86PipelinerHooks) -> bool {
for ii in self.current_ii..=self.max_ii {
self.attempts += 1;
if self.try_schedule(ii, dag, hooks) {
self.succeeded = true;
self.current_ii = ii;
return true;
}
}
self.succeeded = false;
false
}
fn try_schedule(&mut self, ii: u32, dag: &X86ScheduleDAG, hooks: &X86PipelinerHooks) -> bool {
let n = dag.nodes.len();
let mut sched = vec![-1; n];
let num_ports = hooks.num_ports as usize;
let resources = hooks.port_resources.clone();
let mut res_table = ModuloReservationTable::new(ii, num_ports, &resources);
let order = dag.priority_order();
for &ni in &order {
let node = &dag.nodes[ni];
let mut early = node.asap;
for &ei in &dag.predecessors[ni] {
let edge = &dag.edges[ei];
let pc = sched[edge.from];
if pc >= 0 {
early = early.max(pc + edge.latency as i32 - edge.distance * ii as i32);
}
}
let mut placed = false;
for offset in 0..(ii as i32 * 2) {
let cycle = early + offset;
if res_table.can_place(node.resource_mask, cycle) {
sched[ni] = cycle;
res_table.reserve(node.resource_mask, cycle);
placed = true;
break;
}
}
if !placed {
return false;
}
}
self.schedule = sched;
true
}
}
impl Default for X86IterativeModuloScheduler {
fn default() -> Self {
Self::new(1, 1, DEFAULT_MAX_II)
}
}
#[derive(Debug, Clone)]
pub struct X86PipeliningCostModel {
pub ii: u32,
pub body_size: usize,
pub kernel_size: usize,
pub prologue_size: usize,
pub epilogue_size: usize,
pub trip_count: u32,
pub max_code_expansion: f64,
pub is_profitable: bool,
pub estimated_speedup: f64,
}
impl X86PipeliningCostModel {
pub fn new(trip_count: u32, body_size: usize, max_expansion: f64) -> Self {
X86PipeliningCostModel {
ii: 0,
body_size,
kernel_size: 0,
prologue_size: 0,
epilogue_size: 0,
trip_count,
max_code_expansion: max_expansion,
is_profitable: false,
estimated_speedup: 1.0,
}
}
pub fn set_sizes(&mut self, ii: u32, kernel: usize, prologue: usize, epilogue: usize) {
self.ii = ii;
self.kernel_size = kernel;
self.prologue_size = prologue;
self.epilogue_size = epilogue;
}
pub fn evaluate(&mut self) {
let original_cost = self.body_size as f64 * self.trip_count as f64;
let num_stages = if self.body_size > 0 {
(self.kernel_size as f64 / self.body_size as f64).ceil() as u32
} else {
1
};
let kernel_iters = if self.trip_count > num_stages {
self.trip_count - num_stages + 1
} else {
1
};
let pipelined_cost = self.prologue_size as f64
+ self.kernel_size as f64 * kernel_iters as f64
+ self.epilogue_size as f64;
let total_expanded = self.prologue_size + self.kernel_size + self.epilogue_size;
let expansion = if self.body_size > 0 {
total_expanded as f64 / self.body_size as f64
} else {
0.0
};
self.is_profitable = pipelined_cost < original_cost && expansion <= self.max_code_expansion;
self.estimated_speedup = if pipelined_cost > 0.0 {
original_cost / pipelined_cost
} else {
1.0
};
}
pub fn get_estimated_speedup(&self) -> f64 {
self.estimated_speedup
}
pub fn print(&self) -> String {
format!(
"CostModel: II={}, trips={}, body={}, kernel={}, prologue={}, epilogue={}, \
profitable={}, speedup={:.2}x",
self.ii,
self.trip_count,
self.body_size,
self.kernel_size,
self.prologue_size,
self.epilogue_size,
self.is_profitable,
self.estimated_speedup
)
}
}
impl Default for X86PipeliningCostModel {
fn default() -> Self {
Self::new(10, 20, MAX_CODE_EXPANSION_FACTOR)
}
}
#[derive(Debug, Clone)]
pub struct X86PipelineVerifier {
pub valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl X86PipelineVerifier {
pub fn new() -> Self {
X86PipelineVerifier {
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn verify(
&mut self,
expansion: &X86KernelExpansion,
schedule: &[(usize, i32, i32)],
dep_graph: &X86DependenceGraph,
ii: u32,
) {
self.errors.clear();
self.warnings.clear();
self.valid = true;
if schedule.len() != dep_graph.node_count() {
self.valid = false;
self.errors.push(format!(
"Schedule len ({}) != dep graph nodes ({})",
schedule.len(),
dep_graph.node_count()
));
}
for edge in &dep_graph.edges {
let fc = schedule.get(edge.from).map(|&(_, c, _)| c).unwrap_or(-1);
let tc = schedule.get(edge.to).map(|&(_, c, _)| c).unwrap_or(-1);
if fc >= 0 && tc >= 0 {
let req = edge.latency as i32 - edge.distance * ii as i32;
if tc < fc + req {
self.valid = false;
self.errors.push(format!(
"Dep violation: {}->{} fc={} tc={} req={}",
edge.from, edge.to, fc, tc, req
));
}
}
}
if expansion.kernel_trip_count == 0 {
self.warnings.push("Kernel trip count is 0".into());
}
}
pub fn is_valid(&self) -> bool {
self.valid
}
pub fn print(&self) -> String {
let mut out = format!("Valid: {}\n", self.valid);
for e in &self.errors {
out.push_str(&format!("ERROR: {}\n", e));
}
for w in &self.warnings {
out.push_str(&format!("WARN: {}\n", w));
}
out
}
}
impl Default for X86PipelineVerifier {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct X86PredicatedPipeliner {
pub base: X86MachinePipeliner,
pub loop_predicate: Option<u32>,
pub enabled: bool,
pub predicated_instrs: Vec<KernelInstruction>,
}
impl X86PredicatedPipeliner {
pub fn new(base: X86MachinePipeliner) -> Self {
X86PredicatedPipeliner {
base,
loop_predicate: None,
enabled: false,
predicated_instrs: Vec::new(),
}
}
pub fn predicate_instructions(
&mut self,
instrs: &[KernelInstruction],
pred_reg: u32,
) -> Vec<KernelInstruction> {
self.loop_predicate = Some(pred_reg);
self.predicated_instrs = instrs
.iter()
.enumerate()
.map(|(i, ki)| {
let mut c = ki.clone();
c.predicate = Some(PredicateInfo {
pred_reg,
sense: true,
trigger_stage: i as u32,
});
c
})
.collect();
self.predicated_instrs.clone()
}
pub fn count_predicated(&self) -> usize {
self.predicated_instrs
.iter()
.filter(|k| k.predicate.is_some())
.count()
}
pub fn clear(&mut self) {
self.loop_predicate = None;
self.predicated_instrs.clear();
}
}
impl Default for X86PredicatedPipeliner {
fn default() -> Self {
Self::new(X86MachinePipeliner::default())
}
}
#[derive(Debug, Clone)]
pub struct X86LoopUnroller {
pub factor: u32,
pub unrolled: bool,
pub unrolled_instrs: Vec<MachineInstr>,
}
impl X86LoopUnroller {
pub fn new(factor: u32) -> Self {
X86LoopUnroller {
factor,
unrolled: false,
unrolled_instrs: Vec::new(),
}
}
pub fn unroll(&mut self, instrs: &[MachineInstr]) -> Vec<MachineInstr> {
let mut result = Vec::with_capacity(instrs.len() * self.factor as usize);
for _ in 0..self.factor {
result.extend(instrs.iter().cloned());
}
self.unrolled_instrs = result.clone();
self.unrolled = true;
result
}
}
impl Default for X86LoopUnroller {
fn default() -> Self {
Self::new(2)
}
}
#[derive(Debug, Clone)]
pub struct X86BranchPredictorInfo {
pub likely_taken: bool,
pub static_hint: Option<bool>,
pub expected_mispredicts: u32,
}
impl X86BranchPredictorInfo {
pub fn new() -> Self {
X86BranchPredictorInfo {
likely_taken: true,
static_hint: None,
expected_mispredicts: 0,
}
}
}
impl Default for X86BranchPredictorInfo {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HazardKind {
ReadAfterWrite,
WriteAfterRead,
WriteAfterWrite,
StructuralPort,
StructuralUnit,
StoreLoadForwarding,
StoreLoadSizeMismatch,
BypassUnavailable,
Control,
MemoryOrdering,
ResourceExhausted,
MicrocodeAssist,
FlagsDependency,
}
#[derive(Debug, Clone)]
pub struct DetectedHazard {
pub producer: usize,
pub consumer: usize,
pub kind: HazardKind,
pub stall_cycles: u32,
pub can_schedule_around: bool,
pub register: Option<u32>,
pub port: Option<u32>,
pub description: String,
}
#[derive(Debug, Clone)]
pub struct HazardCheckResult {
pub hazards: Vec<DetectedHazard>,
pub can_issue: bool,
pub stall_needed: u32,
}
#[derive(Debug, Clone)]
struct InFlightInstruction {
node_idx: usize,
issue_cycle: i32,
latency: u32,
defines_reg: Option<u32>,
reads_regs: Vec<u32>,
resource_mask: u64,
opcode: u32,
is_store: bool,
is_load: bool,
writes_flags: bool,
reads_flags: bool,
}
#[derive(Debug, Clone)]
pub struct X86SchedulerHazard {
pub hooks: X86PipelinerHooks,
in_flight: Vec<InFlightInstruction>,
resource_usage: Vec<BTreeMap<u32, u32>>,
max_depth: u32,
current_cycle: i32,
pub detected_hazards: Vec<DetectedHazard>,
pub total_hazards_detected: u64,
pub total_data_hazards: u64,
pub total_structural_hazards: u64,
}
impl X86SchedulerHazard {
pub fn new(hooks: X86PipelinerHooks) -> Self {
X86SchedulerHazard {
hooks,
in_flight: Vec::new(),
resource_usage: Vec::new(),
max_depth: MAX_PIPELINE_DEPTH,
current_cycle: 0,
detected_hazards: Vec::new(),
total_hazards_detected: 0,
total_data_hazards: 0,
total_structural_hazards: 0,
}
}
pub fn advance_cycle(&mut self, cycle: i32) {
self.current_cycle = cycle;
self.in_flight
.retain(|instr| instr.issue_cycle + instr.latency as i32 > cycle);
while self.resource_usage.len() > self.max_depth as usize {
self.resource_usage.remove(0);
}
}
pub fn record_issue(
&mut self,
node_idx: usize,
cycle: i32,
opcode: u32,
defines_reg: Option<u32>,
reads_regs: &[u32],
resource_mask: u64,
) {
let latency = self.hooks.get_latency(opcode);
let is_store = is_store_opcode(opcode);
let is_load = is_load_opcode(opcode);
let writes_flags = is_alu_opcode(opcode) || is_compare_opcode(opcode);
let reads_flags = is_branch_opcode(opcode);
self.in_flight.push(InFlightInstruction {
node_idx,
issue_cycle: cycle,
latency,
defines_reg,
reads_regs: reads_regs.to_vec(),
resource_mask,
opcode,
is_store,
is_load,
writes_flags,
reads_flags,
});
while self.resource_usage.len() <= cycle as usize {
self.resource_usage.push(BTreeMap::new());
}
for r in 0..64 {
if (resource_mask >> r) & 1 == 1 {
*self.resource_usage[cycle as usize].entry(r).or_insert(0) += 1;
}
}
}
pub fn check_issue(
&mut self,
opcode: u32,
defines_reg: Option<u32>,
reads_regs: &[u32],
resource_mask: u64,
cycle: i32,
) -> HazardCheckResult {
self.detected_hazards.clear();
let mut can_issue = true;
let mut stall_needed = 0u32;
for instr in &self.in_flight {
if let Some(def_reg) = instr.defines_reg {
for &read_reg in reads_regs {
if read_reg == def_reg {
let latency_left = (instr.issue_cycle + instr.latency as i32) - cycle;
if latency_left > 0 {
can_issue = false;
stall_needed = stall_needed.max(latency_left as u32);
self.detected_hazards.push(DetectedHazard {
producer: instr.node_idx,
consumer: 0,
kind: HazardKind::ReadAfterWrite,
stall_cycles: latency_left as u32,
can_schedule_around: latency_left <= 1,
register: Some(def_reg),
port: None,
description: format!(
"RAW: r{} defined at +{} not ready until +{}",
def_reg,
instr.issue_cycle,
instr.issue_cycle + instr.latency as i32
),
});
self.total_data_hazards += 1;
}
}
}
}
}
if let Some(def_reg) = defines_reg {
for instr in &self.in_flight {
for &read_reg in &instr.reads_regs {
if read_reg == def_reg && instr.issue_cycle < cycle {
self.detected_hazards.push(DetectedHazard {
producer: 0,
consumer: instr.node_idx,
kind: HazardKind::WriteAfterRead,
stall_cycles: 0,
can_schedule_around: true,
register: Some(def_reg),
port: None,
description: format!(
"WAR: r{} being defined conflicts with in-flight read",
def_reg
),
});
}
}
}
}
if let Some(def_reg) = defines_reg {
for instr in &self.in_flight {
if instr.defines_reg == Some(def_reg) {
self.detected_hazards.push(DetectedHazard {
producer: instr.node_idx,
consumer: 0,
kind: HazardKind::WriteAfterWrite,
stall_cycles: 0,
can_schedule_around: true,
register: Some(def_reg),
port: None,
description: format!("WAW: r{} defined twice in flight", def_reg),
});
}
}
}
let port_usage = self
.resource_usage
.get(cycle as usize)
.cloned()
.unwrap_or_default();
for r in 0..64 {
if (resource_mask >> r) & 1 == 1 {
let current = port_usage.get(&r).copied().unwrap_or(0);
let max_allowed = self
.hooks
.port_resources
.get(r as usize)
.copied()
.unwrap_or(1);
if current >= max_allowed {
can_issue = false;
stall_needed = stall_needed.max(1);
self.detected_hazards.push(DetectedHazard {
producer: 0,
consumer: 0,
kind: HazardKind::StructuralPort,
stall_cycles: 1,
can_schedule_around: false,
register: None,
port: Some(r),
description: format!(
"Structural: port {} at {}x (max {})",
r, current, max_allowed
),
});
self.total_structural_hazards += 1;
}
}
}
let is_load = is_load_opcode(opcode);
if is_load {
for instr in &self.in_flight {
if instr.is_store {
let store_age = cycle - instr.issue_cycle;
if store_age < 4 {
self.detected_hazards.push(DetectedHazard {
producer: instr.node_idx,
consumer: 0,
kind: HazardKind::StoreLoadForwarding,
stall_cycles: if store_age < 3 { 0 } else { 1 },
can_schedule_around: true,
register: None,
port: None,
description: format!(
"Store-to-load forwarding: store at +{} load at +{}",
instr.issue_cycle, cycle
),
});
}
}
}
}
let writes_flags = is_alu_opcode(opcode) || is_compare_opcode(opcode);
let reads_flags = is_branch_opcode(opcode);
if reads_flags {
for instr in &self.in_flight {
if instr.writes_flags {
let latency_left = (instr.issue_cycle + instr.latency as i32) - cycle;
if latency_left > 0 {
can_issue = false;
stall_needed = stall_needed.max(latency_left as u32);
self.detected_hazards.push(DetectedHazard {
producer: instr.node_idx,
consumer: 0,
kind: HazardKind::FlagsDependency,
stall_cycles: latency_left as u32,
can_schedule_around: latency_left == 1,
register: None,
port: None,
description: format!(
"Flags: EFLAGS written at +{} not ready for branch at +{}",
instr.issue_cycle, cycle
),
});
}
}
}
}
self.total_hazards_detected += self.detected_hazards.len() as u64;
HazardCheckResult {
hazards: self.detected_hazards.clone(),
can_issue,
stall_needed,
}
}
pub fn min_separation(
&self,
producer_opcode: u32,
consumer_opcode: u32,
producer_defines: Option<u32>,
consumer_reads: &[u32],
) -> u32 {
let producer_lat = self.hooks.get_latency(producer_opcode);
if let Some(def) = producer_defines {
if consumer_reads.contains(&def) {
if is_alu_opcode(producer_opcode) && is_alu_opcode(consumer_opcode) {
return 1; }
if is_load_opcode(producer_opcode) {
return producer_lat; }
return producer_lat;
}
}
let p_mask = self.hooks.get_resource_mask(producer_opcode);
let c_mask = self.hooks.get_resource_mask(consumer_opcode);
if p_mask & c_mask != 0 {
return 1; }
0 }
pub fn bypass_available(&self, producer_opcode: u32, consumer_opcode: u32) -> bool {
if is_alu_opcode(producer_opcode) && is_alu_opcode(consumer_opcode) {
return true;
}
if is_alu_opcode(producer_opcode) && is_store_opcode(consumer_opcode) {
return true;
}
if is_load_opcode(producer_opcode) && is_alu_opcode(consumer_opcode) {
return false;
}
if is_load_opcode(producer_opcode) && is_store_opcode(consumer_opcode) {
return false;
}
false
}
pub fn can_forward_store_to_load(&self, store_cycle: i32, load_cycle: i32) -> bool {
let distance = load_cycle - store_cycle;
(1..=3).contains(&distance)
}
pub fn reset(&mut self) {
self.in_flight.clear();
self.resource_usage.clear();
self.detected_hazards.clear();
self.current_cycle = 0;
}
pub fn in_flight_count(&self) -> usize {
self.in_flight.len()
}
pub fn print_stats(&self) -> String {
format!(
"X86SchedulerHazard: in_flight={}, hazards_total={}, data={}, structural={}",
self.in_flight.len(),
self.total_hazards_detected,
self.total_data_hazards,
self.total_structural_hazards,
)
}
}
impl Default for X86SchedulerHazard {
fn default() -> Self {
Self::new(X86PipelinerHooks::default())
}
}
#[derive(Debug, Clone)]
pub struct HazardAwareScheduler {
pub hazard: X86SchedulerHazard,
pub schedule: Vec<(usize, i32)>,
pub placed: HashSet<usize>,
pub resource_usage: BTreeMap<i32, Vec<u64>>,
}
impl HazardAwareScheduler {
pub fn new(hooks: X86PipelinerHooks) -> Self {
HazardAwareScheduler {
hazard: X86SchedulerHazard::new(hooks),
schedule: Vec::new(),
placed: HashSet::new(),
resource_usage: BTreeMap::new(),
}
}
pub fn schedule_node(
&mut self,
node_idx: usize,
opcode: u32,
defines_reg: Option<u32>,
reads_regs: &[u32],
resource_mask: u64,
earliest_cycle: i32,
latest_cycle: i32,
) -> Option<i32> {
for cycle in earliest_cycle..=latest_cycle {
let check =
self.hazard
.check_issue(opcode, defines_reg, reads_regs, resource_mask, cycle);
if check.can_issue {
self.hazard.record_issue(
node_idx,
cycle,
opcode,
defines_reg,
reads_regs,
resource_mask,
);
self.placed.insert(node_idx);
self.schedule.push((node_idx, cycle));
self.resource_usage
.entry(cycle)
.or_default()
.push(resource_mask);
return Some(cycle);
}
}
None
}
pub fn get_schedule(&self) -> &[(usize, i32)] {
&self.schedule
}
pub fn print_schedule(&self) -> String {
let mut out = String::from("HazardAwareScheduler:\n");
let mut sorted = self.schedule.clone();
sorted.sort_by_key(|(_, c)| *c);
for (node, cycle) in &sorted {
out.push_str(&format!(" node {} @ cycle {}\n", node, cycle));
}
out
}
}
impl Default for HazardAwareScheduler {
fn default() -> Self {
Self::new(X86PipelinerHooks::default())
}
}
#[derive(Debug, Clone)]
pub struct ExecPort {
pub index: u32,
pub name: String,
pub pipes: u32,
pub can_int_alu: bool,
pub can_int_mul: bool,
pub can_int_div: bool,
pub can_branch: bool,
pub can_fp_add: bool,
pub can_fp_mul: bool,
pub can_fp_div: bool,
pub can_load_addr: bool,
pub can_store_addr: bool,
pub can_store_data: bool,
pub can_vector: bool,
pub pipeline_depth: u32,
pub fully_pipelined: bool,
}
impl ExecPort {
pub fn new(index: u32, name: &str) -> Self {
ExecPort {
index,
name: name.to_string(),
pipes: 1,
can_int_alu: false,
can_int_mul: false,
can_int_div: false,
can_branch: false,
can_fp_add: false,
can_fp_mul: false,
can_fp_div: false,
can_load_addr: false,
can_store_addr: false,
can_store_data: false,
can_vector: false,
pipeline_depth: 1,
fully_pipelined: true,
}
}
pub fn can_execute(&self, opcode: u32) -> bool {
if is_alu_opcode(opcode) {
return self.can_int_alu;
}
if is_multiply_opcode(opcode) {
return self.can_int_mul;
}
if is_divide_opcode(opcode) {
return self.can_int_div;
}
if is_branch_opcode(opcode) {
return self.can_branch;
}
if is_load_opcode(opcode) {
return self.can_load_addr;
}
if is_store_opcode(opcode) {
return self.can_store_addr || self.can_store_data;
}
if (100..200).contains(&opcode) {
return self.can_fp_add;
}
if (200..300).contains(&opcode) {
return self.can_fp_mul;
}
if (300..400).contains(&opcode) {
return self.can_fp_div;
}
if (1000..3000).contains(&opcode) {
return self.can_vector;
}
false
}
}
#[derive(Debug, Clone, Default)]
pub struct CycleResourceUsage {
pub port_usage: u64,
pub port_counts: Vec<u32>,
pub loads_issued: u32,
pub stores_issued: u32,
pub branches_issued: u32,
pub alu_ops_issued: u32,
pub fp_ops_issued: u32,
pub total_uops: u32,
}
impl CycleResourceUsage {
pub fn reset(&mut self) {
self.port_usage = 0;
for c in &mut self.port_counts {
*c = 0;
}
self.loads_issued = 0;
self.stores_issued = 0;
self.branches_issued = 0;
self.alu_ops_issued = 0;
self.fp_ops_issued = 0;
self.total_uops = 0;
}
pub fn utilization(&self, max_ports: u32) -> f64 {
if max_ports == 0 {
return 0.0;
}
self.port_usage.count_ones() as f64 / max_ports as f64
}
}
#[derive(Debug, Clone)]
pub struct X86ResourceModel {
pub hooks: X86PipelinerHooks,
pub ports: Vec<ExecPort>,
pub cycle_usage: BTreeMap<i32, CycleResourceUsage>,
pub reservation: Vec<Vec<u64>>,
pub issue_width: u32,
pub rob_capacity: u32,
pub load_buffer_capacity: u32,
pub store_buffer_capacity: u32,
pub in_flight_loads: u32,
pub in_flight_stores: u32,
pub total_instructions: u64,
pub resource_exhausted_cycles: u64,
}
impl X86ResourceModel {
pub fn new(hooks: X86PipelinerHooks) -> Self {
let ports = Self::build_port_model(&hooks);
X86ResourceModel {
hooks: hooks.clone(),
ports,
cycle_usage: BTreeMap::new(),
reservation: Vec::new(),
issue_width: hooks.issue_width,
rob_capacity: hooks.rob_size,
load_buffer_capacity: hooks.load_buffer_size,
store_buffer_capacity: hooks.store_buffer_size,
in_flight_loads: 0,
in_flight_stores: 0,
total_instructions: 0,
resource_exhausted_cycles: 0,
}
}
fn build_port_model(hooks: &X86PipelinerHooks) -> Vec<ExecPort> {
let mut ports = Vec::new();
for i in 0..hooks.num_ports {
let name = hooks
.port_names
.get(i as usize)
.cloned()
.unwrap_or_else(|| format!("Port{}", i));
let mut port = ExecPort::new(i, &name);
port.pipes = hooks.port_resources.get(i as usize).copied().unwrap_or(1);
let mask_bit = 1u64 << i;
let alu_mask = hooks.get_resource_mask(2); let mul_mask = hooks.get_resource_mask(4);
let div_mask = hooks.get_resource_mask(5);
let load_mask = hooks.get_resource_mask(1);
let store_mask = hooks.get_resource_mask(STORE_OPCODE);
let branch_mask = hooks.get_resource_mask(15);
let fp_add_mask = hooks.get_resource_mask(100);
let fp_mul_mask = hooks.get_resource_mask(200);
let fp_div_mask = hooks.get_resource_mask(300);
port.can_int_alu = (alu_mask & mask_bit) != 0;
port.can_int_mul = (mul_mask & mask_bit) != 0;
port.can_int_div = (div_mask & mask_bit) != 0;
port.can_load_addr = (load_mask & mask_bit) != 0;
port.can_store_addr = (store_mask & mask_bit) != 0;
port.can_store_data = (store_mask & mask_bit) != 0;
port.can_branch = (branch_mask & mask_bit) != 0;
port.can_fp_add = (fp_add_mask & mask_bit) != 0;
port.can_fp_mul = (fp_mul_mask & mask_bit) != 0;
port.can_fp_div = (fp_div_mask & mask_bit) != 0;
port.can_vector = port.can_fp_add || port.can_fp_mul;
port.pipeline_depth = if port.can_int_div {
hooks.get_latency(5) } else if port.can_fp_div {
hooks.get_latency(300)
} else if port.can_int_mul {
hooks.get_latency(4)
} else {
1
};
port.fully_pipelined = !port.can_int_div && !port.can_fp_div;
ports.push(port);
}
ports
}
pub fn can_accommodate(&self, resource_mask: u64, cycle: i32) -> bool {
if cycle < 0 {
return false;
}
let c = cycle as usize;
if c >= self.reservation.len() {
return true; }
for r in 0..64 {
if (resource_mask >> r) & 1 == 1 {
if r < self.reservation[c].len() {
if self.reservation[c][r]
>= self.hooks.port_resources.get(r).copied().unwrap_or(1) as u64
{
return false;
}
}
}
}
true
}
pub fn reserve(&mut self, resource_mask: u64, cycle: i32, opcode: u32) -> bool {
if !self.can_accommodate(resource_mask, cycle) {
return false;
}
let c = cycle as usize;
while self.reservation.len() <= c {
self.reservation
.push(vec![0u64; self.hooks.num_ports as usize]);
}
for r in 0..64 {
if (resource_mask >> r) & 1 == 1 {
if r < self.reservation[c].len() {
self.reservation[c][r] += 1;
}
}
}
let usage = self.cycle_usage.entry(cycle).or_insert_with(|| {
let mut cu = CycleResourceUsage::default();
cu.port_counts = vec![0; self.hooks.num_ports as usize];
cu
});
usage.port_usage |= resource_mask;
usage.total_uops += self.hooks.get_uop_count(opcode);
if is_load_opcode(opcode) {
usage.loads_issued += 1;
self.in_flight_loads += 1;
}
if is_store_opcode(opcode) {
usage.stores_issued += 1;
self.in_flight_stores += 1;
}
if is_branch_opcode(opcode) {
usage.branches_issued += 1;
}
if is_alu_opcode(opcode) {
usage.alu_ops_issued += 1;
}
if (100..400).contains(&opcode) {
usage.fp_ops_issued += 1;
}
for r in 0..64 {
if (resource_mask >> r) & 1 == 1 && r < usage.port_counts.len() {
usage.port_counts[r] += 1;
}
}
self.total_instructions += 1;
true
}
pub fn release(&mut self, resource_mask: u64, cycle: i32, opcode: u32) {
let c = cycle as usize;
if c < self.reservation.len() {
for r in 0..64 {
if (resource_mask >> r) & 1 == 1 {
if r < self.reservation[c].len() && self.reservation[c][r] > 0 {
self.reservation[c][r] -= 1;
}
}
}
}
if is_load_opcode(opcode) && self.in_flight_loads > 0 {
self.in_flight_loads -= 1;
}
if is_store_opcode(opcode) && self.in_flight_stores > 0 {
self.in_flight_stores -= 1;
}
}
pub fn compute_res_mii(&self, opcodes: &[u32]) -> u32 {
if opcodes.is_empty() {
return 1;
}
let mut total_port_usage = vec![0u64; self.hooks.num_ports as usize];
for &opcode in opcodes {
let mask = self.hooks.get_resource_mask(opcode);
for r in 0..64 {
if (mask >> r) & 1 == 1 && r < total_port_usage.len() {
total_port_usage[r] += 1;
}
}
}
let mut max_ratio = 0f64;
for r in 0..total_port_usage.len() {
let capacity = self.hooks.port_resources.get(r).copied().unwrap_or(1) as f64;
if capacity > 0.0 {
let ratio = total_port_usage[r] as f64 / capacity;
max_ratio = max_ratio.max(ratio);
}
}
(max_ratio.ceil() as u32).max(1)
}
pub fn issue_utilization(&self, cycle: i32) -> f64 {
self.cycle_usage
.get(&cycle)
.map(|u| u.total_uops as f64 / self.issue_width as f64)
.unwrap_or(0.0)
}
pub fn rob_full(&self) -> bool {
self.total_instructions >= self.rob_capacity as u64
}
pub fn load_buffer_full(&self) -> bool {
self.in_flight_loads >= self.load_buffer_capacity
}
pub fn store_buffer_full(&self) -> bool {
self.in_flight_stores >= self.store_buffer_capacity
}
pub fn reset(&mut self) {
self.cycle_usage.clear();
self.reservation.clear();
self.in_flight_loads = 0;
self.in_flight_stores = 0;
self.total_instructions = 0;
self.resource_exhausted_cycles = 0;
}
pub fn get_ports_for_opcode(&self, opcode: u32) -> Vec<u32> {
self.ports
.iter()
.filter(|p| p.can_execute(opcode))
.map(|p| p.index)
.collect()
}
pub fn utilization_summary(&self) -> String {
let total_cycles = self.cycle_usage.len();
let avg_util: f64 = if total_cycles > 0 {
self.cycle_usage
.values()
.map(|u| u.utilization(self.issue_width))
.sum::<f64>()
/ total_cycles as f64
} else {
0.0
};
format!(
"X86ResourceModel: ports={}, issue_width={}, ROB={}, loads_inflight={}, stores_inflight={}, \
total_instrs={}, cycles={}, avg_util={:.1}%, exhausted_cycles={}",
self.ports.len(),
self.issue_width,
self.rob_capacity,
self.in_flight_loads,
self.in_flight_stores,
self.total_instructions,
total_cycles,
avg_util * 100.0,
self.resource_exhausted_cycles,
)
}
pub fn print_ports(&self) -> String {
let mut out = String::from("X86ResourceModel Ports:\n");
for port in &self.ports {
out.push_str(&format!(
" {}: pipes={}, alu={}, mul={}, div={}, branch={}, load={}, store={}, fp={}, vec={}, depth={}, pipelined={}\n",
port.name,
port.pipes,
port.can_int_alu,
port.can_int_mul,
port.can_int_div,
port.can_branch,
port.can_load_addr,
port.can_store_addr || port.can_store_data,
port.can_fp_add || port.can_fp_mul,
port.can_vector,
port.pipeline_depth,
port.fully_pipelined,
));
}
out
}
}
impl Default for X86ResourceModel {
fn default() -> Self {
Self::new(X86PipelinerHooks::default())
}
}
#[derive(Debug, Clone)]
pub struct X86DependenceAnalysis {
pub dep_graph: X86DependenceGraph,
pub distance_vectors: Vec<(usize, usize, i32)>,
pub reg_distances: HashMap<(u32, u32), i32>,
pub loop_independent: Vec<usize>,
pub loop_carried: Vec<usize>,
pub control_edges: Vec<usize>,
pub alias_pairs: Vec<(usize, usize, bool)>,
}
impl X86DependenceAnalysis {
pub fn new(hooks: X86PipelinerHooks) -> Self {
X86DependenceAnalysis {
dep_graph: X86DependenceGraph::new(hooks),
distance_vectors: Vec::new(),
reg_distances: HashMap::new(),
loop_independent: Vec::new(),
loop_carried: Vec::new(),
control_edges: Vec::new(),
alias_pairs: Vec::new(),
}
}
pub fn analyze(
&mut self,
loop_blocks: &[usize],
mf: &MachineFunction,
loop_analysis: &X86LoopAnalysis,
header: usize,
) {
self.dep_graph.build(loop_blocks, mf);
self.loop_independent.clear();
self.loop_carried.clear();
self.control_edges.clear();
for (ei, edge) in self.dep_graph.edges.iter().enumerate() {
match edge.kind {
DepKind::Control => self.control_edges.push(ei),
_ => {
if edge.is_carried {
self.loop_carried.push(ei);
} else {
self.loop_independent.push(ei);
}
}
}
}
self.distance_vectors.clear();
if let Some(mem_deps) = loop_analysis.memory_deps.get(&header) {
for md in mem_deps {
self.distance_vectors.push((md.from, md.to, md.distance));
}
}
self.reg_distances.clear();
for edge in &self.dep_graph.edges {
if let Some(reg) = edge.register {
let key = (reg, edge.from as u32);
self.reg_distances
.entry(key)
.and_modify(|d| *d = (*d).max(edge.distance))
.or_insert(edge.distance);
}
}
}
pub fn iteration_distance(&self, from: usize, to: usize) -> i32 {
for edge in &self.dep_graph.edges {
if edge.from == from && edge.to == to && edge.is_carried {
return edge.distance;
}
}
0
}
pub fn may_alias(&self, a: usize, b: usize) -> bool {
self.alias_pairs
.iter()
.any(|&(x, y, may)| (x == a && y == b || x == b && y == a) && may)
}
pub fn carried_count(&self) -> usize {
self.loop_carried.len()
}
pub fn independent_count(&self) -> usize {
self.loop_independent.len()
}
pub fn print_summary(&self) -> String {
format!(
"X86DependenceAnalysis: nodes={}, edges={}, loop_independent={}, loop_carried={}, control={}, dist_vectors={}",
self.dep_graph.node_count(),
self.dep_graph.edge_count(),
self.loop_independent.len(),
self.loop_carried.len(),
self.control_edges.len(),
self.distance_vectors.len(),
)
}
}
impl Default for X86DependenceAnalysis {
fn default() -> Self {
Self::new(X86PipelinerHooks::default())
}
}
#[derive(Debug, Clone)]
pub struct X86LoopPipeliner {
pub hooks: X86PipelinerHooks,
pub loop_analysis: X86LoopAnalysis,
pub dep_analysis: X86DependenceAnalysis,
pub scheduler: Option<X86ModuloScheduler>,
pub hazard: X86SchedulerHazard,
pub resource_model: X86ResourceModel,
pub cost_model: X86PipeliningCostModel,
pub unroller: X86LoopUnroller,
pub expansion: Option<X86KernelExpansion>,
pub pipelined: bool,
pub achieved_ii: u32,
}
impl X86LoopPipeliner {
pub fn new(sched_model: X86SchedModel) -> Self {
let hooks = X86PipelinerHooks::new(sched_model);
X86LoopPipeliner {
hooks: hooks.clone(),
loop_analysis: X86LoopAnalysis::new(),
dep_analysis: X86DependenceAnalysis::new(hooks.clone()),
scheduler: None,
hazard: X86SchedulerHazard::new(hooks.clone()),
resource_model: X86ResourceModel::new(hooks.clone()),
cost_model: X86PipeliningCostModel::default(),
unroller: X86LoopUnroller::default(),
expansion: None,
pipelined: false,
achieved_ii: 0,
}
}
pub fn pipeline_loop(&mut self, loop_info: &NaturalLoop, mf: &mut MachineFunction) -> bool {
self.pipelined = false;
let header = loop_info.header;
if !self.loop_analysis.loops.iter().any(|l| l.header == header) {
self.loop_analysis.analyze(mf);
}
let trip_count = self.loop_analysis.get_trip_count(header);
let has_carried = self.loop_analysis.has_carried_deps(header);
self.cost_model.trip_count = trip_count;
if !self.hooks.should_pipeline(trip_count, 10, has_carried) {
return false;
}
let loop_blocks: Vec<usize> = loop_info.blocks.iter().copied().collect();
if loop_blocks.is_empty() {
return false;
}
self.dep_analysis
.analyze(&loop_blocks, mf, &self.loop_analysis, header);
self.dep_analysis.dep_graph.compute_asap();
self.dep_analysis
.dep_graph
.compute_alap(self.dep_analysis.dep_graph.node_count() as i32 * 4);
self.dep_analysis.dep_graph.compute_heights();
let mut sched_dag = X86ScheduleDAG::new(self.hooks.clone());
sched_dag.build_from_dep_graph(&self.dep_analysis.dep_graph);
let opcodes: Vec<u32> = self
.dep_analysis
.dep_graph
.nodes
.iter()
.map(|n| n.opcode)
.collect();
let res_mii = self.resource_model.compute_res_mii(&opcodes);
let mut scheduler = X86ModuloScheduler::new(
self.hooks.clone(),
sched_dag.clone(),
self.dep_analysis.dep_graph.clone(),
);
scheduler.res_mii = res_mii;
if !scheduler.schedule() {
return false;
}
let ii = scheduler.ii;
let num_stages = scheduler.num_stages();
self.hazard.reset();
let sched = scheduler.get_schedule();
let mut schedule_valid = true;
for &(ni, cycle, _stage) in &sched {
let node = &self.dep_analysis.dep_graph.nodes[ni];
let check = self.hazard.check_issue(
node.opcode,
node.defines_reg,
&node.uses_regs,
node.resource_mask,
cycle,
);
if !check.can_issue && cycle > 0 {
schedule_valid = false;
break;
}
self.hazard.record_issue(
ni,
cycle,
node.opcode,
node.defines_reg,
&node.uses_regs,
node.resource_mask,
);
}
if !schedule_valid {
}
let loop_instrs: Vec<MachineInstr> = self
.dep_analysis
.dep_graph
.nodes
.iter()
.map(|n| {
mf.blocks
.get(n.block_idx)
.and_then(|b| b.instructions.get(n.instr_idx))
.cloned()
.unwrap_or_else(|| MachineInstr::new(0))
})
.collect();
let mut expansion = X86KernelExpansion::new(ii, num_stages, trip_count, self.hooks.clone());
expansion.expand(&sched, &loop_instrs);
self.cost_model.set_sizes(
ii,
expansion.kernel.len(),
expansion.prologue.iter().map(|b| b.len()).sum(),
expansion.epilogue.iter().map(|b| b.len()).sum(),
);
self.cost_model.body_size = loop_instrs.len();
self.cost_model.evaluate();
if !self.cost_model.is_profitable {
return false;
}
let prologue = expansion.generate_prologue_blocks();
let kernel = expansion.generate_kernel_block();
let epilogue = expansion.generate_epilogue_blocks();
let mut indices: Vec<usize> = loop_info.blocks.iter().copied().collect();
indices.sort_unstable_by(|a, b| b.cmp(a));
for &idx in &indices {
if idx < mf.blocks.len() {
mf.blocks.remove(idx);
}
}
let insert_pos = loop_info
.preheader
.map(|p| p + 1)
.unwrap_or(loop_info.header)
.min(mf.blocks.len());
let mut pos = insert_pos;
for block in prologue {
mf.blocks.insert(pos, block);
pos += 1;
}
mf.blocks.insert(pos, kernel);
pos += 1;
for block in epilogue {
mf.blocks.insert(pos, block);
pos += 1;
}
self.pipelined = true;
self.achieved_ii = ii;
self.scheduler = Some(scheduler);
self.expansion = Some(expansion);
true
}
pub fn print_stats(&self) -> String {
let mut out = String::from("X86LoopPipeliner Stats:\n");
out.push_str(&format!(" Pipelined: {}\n", self.pipelined));
out.push_str(&format!(" II: {}\n", self.achieved_ii));
out.push_str(&format!(" {}\n", self.dep_analysis.print_summary()));
out.push_str(&format!(" {}\n", self.hazard.print_stats()));
out.push_str(&format!(
" {}\n",
self.resource_model.utilization_summary()
));
out.push_str(&format!(" {}\n", self.cost_model.print()));
out
}
}
impl Default for X86LoopPipeliner {
fn default() -> Self {
Self::new(X86SchedModel::Generic)
}
}
#[derive(Debug, Clone)]
pub struct LiveRegisterTracker {
pub live_ranges: HashMap<u32, (i32, i32)>,
pub live_at_cycle: BTreeMap<i32, HashSet<u32>>,
pub max_live_regs: u32,
pub reg_file_size: u32,
}
impl LiveRegisterTracker {
pub fn new(reg_file_size: u32) -> Self {
LiveRegisterTracker {
live_ranges: HashMap::new(),
live_at_cycle: BTreeMap::new(),
max_live_regs: 0,
reg_file_size,
}
}
pub fn record_def(&mut self, vreg: u32, cycle: i32) {
let entry = self.live_ranges.entry(vreg).or_insert((cycle, cycle));
entry.0 = entry.0.min(cycle);
}
pub fn record_use(&mut self, vreg: u32, cycle: i32) {
let entry = self.live_ranges.entry(vreg).or_insert((cycle, cycle));
entry.1 = entry.1.max(cycle);
}
pub fn compute_live_sets(&mut self) {
self.live_at_cycle.clear();
self.max_live_regs = 0;
for (&vreg, &(def, last_use)) in &self.live_ranges {
for cycle in def..=last_use {
self.live_at_cycle.entry(cycle).or_default().insert(vreg);
}
}
for set in self.live_at_cycle.values() {
self.max_live_regs = self.max_live_regs.max(set.len() as u32);
}
}
pub fn pressure_at(&self, cycle: i32) -> u32 {
self.live_at_cycle
.get(&cycle)
.map(|s| s.len() as u32)
.unwrap_or(0)
}
pub fn exceeds_pressure(&self, cycle: i32, additional: u32) -> bool {
self.pressure_at(cycle) + additional > self.reg_file_size
}
pub fn reset(&mut self) {
self.live_ranges.clear();
self.live_at_cycle.clear();
self.max_live_regs = 0;
}
}
impl Default for LiveRegisterTracker {
fn default() -> Self {
Self::new(16) }
}
#[derive(Debug, Clone)]
pub struct X86RegPressureAwareScheduler {
pub hooks: X86PipelinerHooks,
pub reg_tracker: LiveRegisterTracker,
pub schedule: Vec<i32>,
pub max_pressure: u32,
pub succeeded: bool,
}
impl X86RegPressureAwareScheduler {
pub fn new(hooks: X86PipelinerHooks) -> Self {
X86RegPressureAwareScheduler {
hooks: hooks.clone(),
reg_tracker: LiveRegisterTracker::new(hooks.max_register_pressure),
schedule: Vec::new(),
max_pressure: hooks.max_register_pressure,
succeeded: false,
}
}
pub fn schedule(&mut self, dag: &X86ScheduleDAG, dep_graph: &X86DependenceGraph) -> bool {
let n = dag.nodes.len();
self.schedule = vec![-1; n];
self.reg_tracker.reset();
self.succeeded = false;
let order = dag.priority_order();
let mut current_cycle = 0i32;
for &ni in &order {
let node = &dag.nodes[ni];
let dep_node = &dep_graph.nodes[ni];
let mut earliest = 0i32;
for &ei in &dag.predecessors[ni] {
let edge = &dag.edges[ei];
let pc = self.schedule[edge.from];
if pc >= 0 {
earliest = earliest.max(pc + edge.latency as i32 - edge.distance);
}
}
let mut placed = false;
for cycle in earliest..(earliest + 100) {
let mut additional_pressure = 0u32;
if dep_node.defines_reg.is_some() {
additional_pressure += 1;
}
for &r in &dep_node.uses_regs {
if !self.reg_tracker.live_ranges.contains_key(&r) {
additional_pressure += 1;
}
}
if !self
.reg_tracker
.exceeds_pressure(cycle, additional_pressure)
{
self.schedule[ni] = cycle;
if let Some(def) = dep_node.defines_reg {
self.reg_tracker.record_def(def, cycle);
self.reg_tracker
.record_use(def, cycle + dep_node.latency as i32 + 10);
}
for &r in &dep_node.uses_regs {
self.reg_tracker.record_use(r, cycle);
}
placed = true;
break;
}
}
if !placed {
return false;
}
current_cycle = current_cycle.max(self.schedule[ni]);
}
self.reg_tracker.compute_live_sets();
self.succeeded = true;
true
}
pub fn get_max_pressure(&self) -> u32 {
self.reg_tracker.max_live_regs
}
pub fn print_stats(&self) -> String {
format!(
"RegPressureScheduler: succeeded={}, max_pressure={}/{}, nodes={}",
self.succeeded,
self.reg_tracker.max_live_regs,
self.max_pressure,
self.schedule.len(),
)
}
}
impl Default for X86RegPressureAwareScheduler {
fn default() -> Self {
Self::new(X86PipelinerHooks::default())
}
}
#[derive(Debug, Clone)]
pub struct X86KernelUnroller {
pub factor: u32,
pub original_nodes: usize,
pub unrolled_instrs: Vec<MachineInstr>,
pub unrolled: bool,
}
impl X86KernelUnroller {
pub fn new(factor: u32) -> Self {
X86KernelUnroller {
factor: factor.max(1),
original_nodes: 0,
unrolled_instrs: Vec::new(),
unrolled: false,
}
}
pub fn select_factor(trip_count: u32, body_size: usize, has_carried_deps: bool) -> u32 {
if trip_count < 8 {
return 1; }
if body_size > 100 {
return 2; }
if has_carried_deps {
if trip_count >= 64 {
return 4;
}
return 2;
}
if body_size <= 16 {
return 4;
}
2
}
pub fn unroll(&mut self, instrs: &[MachineInstr]) -> Vec<MachineInstr> {
self.original_nodes = instrs.len();
self.unrolled_instrs.clear();
if self.factor <= 1 {
self.unrolled_instrs = instrs.to_vec();
return self.unrolled_instrs.clone();
}
let mut result = Vec::with_capacity(instrs.len() * self.factor as usize);
for iter in 0..self.factor {
for instr in instrs {
let mut mi = instr.clone();
if let Some(ref mut def) = mi.def {
*def = *def + iter * 1000; }
for op in &mut mi.operands {
if let MachineOperand::Reg(ref mut r) = op {
let base_r = *r % 1000;
if base_r != *r && base_r < instrs.len() as u32 {
*r = base_r + iter * 1000;
}
}
}
result.push(mi);
}
}
self.unrolled_instrs = result.clone();
self.unrolled = true;
result
}
pub fn generate_epilogue(
&self,
original_instrs: &[MachineInstr],
remaining_iters: u32,
) -> Vec<MachineInstr> {
let mut epilogue = Vec::new();
for _ in 0..remaining_iters {
epilogue.extend(original_instrs.iter().cloned());
}
epilogue
}
pub fn print_stats(&self) -> String {
format!(
"X86KernelUnroller: factor={}, original={}, unrolled={}, performed={}",
self.factor,
self.original_nodes,
self.unrolled_instrs.len(),
self.unrolled,
)
}
}
impl Default for X86KernelUnroller {
fn default() -> Self {
Self::new(2)
}
}
#[derive(Debug, Clone)]
pub struct X86PostRAPipeliner {
pub hooks: X86PipelinerHooks,
pub hazard: X86SchedulerHazard,
pub resource_model: X86ResourceModel,
pub blocks_processed: usize,
pub instructions_reordered: usize,
pub cycles_saved: u32,
pub enabled: bool,
}
impl X86PostRAPipeliner {
pub fn new(sched_model: X86SchedModel) -> Self {
let hooks = X86PipelinerHooks::new(sched_model);
X86PostRAPipeliner {
hooks: hooks.clone(),
hazard: X86SchedulerHazard::new(hooks.clone()),
resource_model: X86ResourceModel::new(hooks),
blocks_processed: 0,
instructions_reordered: 0,
cycles_saved: 0,
enabled: true,
}
}
pub fn run_on_function(&mut self, mf: &mut MachineFunction) -> bool {
if !self.enabled {
return false;
}
let mut modified = false;
for block_idx in 0..mf.blocks.len() {
if self.reorder_block(&mut mf.blocks[block_idx]) {
modified = true;
self.blocks_processed += 1;
}
}
modified
}
fn reorder_block(&mut self, block: &mut MachineBasicBlock) -> bool {
let n = block.instructions.len();
if n < 2 {
return false;
}
self.hazard.reset();
self.resource_model.reset();
let mut new_order: Vec<MachineInstr> = Vec::with_capacity(n);
let mut scheduled: HashSet<usize> = HashSet::new();
for cycle in 0..(n as i32 * 2) {
self.hazard.advance_cycle(cycle);
let mut best_idx: Option<usize> = None;
let mut best_score = -1i32;
for (i, instr) in block.instructions.iter().enumerate() {
if scheduled.contains(&i) {
continue;
}
let reads: Vec<u32> = instr
.operands
.iter()
.filter_map(|op| match op {
MachineOperand::Reg(r) => Some(*r),
_ => None,
})
.collect();
let mask = self.hooks.get_resource_mask(instr.opcode);
let check = self
.hazard
.check_issue(instr.opcode, instr.def, &reads, mask, cycle);
if check.can_issue {
let latency = self.hooks.get_latency(instr.opcode) as i32;
let port_count = mask.count_ones() as i32;
let score = latency * 10 + port_count;
if score > best_score {
best_score = score;
best_idx = Some(i);
}
}
}
if let Some(idx) = best_idx {
let instr = &block.instructions[idx];
let reads: Vec<u32> = instr
.operands
.iter()
.filter_map(|op| match op {
MachineOperand::Reg(r) => Some(*r),
_ => None,
})
.collect();
let mask = self.hooks.get_resource_mask(instr.opcode);
self.hazard
.record_issue(idx, cycle, instr.opcode, instr.def, &reads, mask);
self.resource_model.reserve(mask, cycle, instr.opcode);
new_order.push(instr.clone());
scheduled.insert(idx);
}
if scheduled.len() == n {
break;
}
}
if scheduled.len() < n {
return false;
}
let original_len = block.instructions.len();
block.instructions = new_order;
self.instructions_reordered += original_len;
if original_len > 4 {
self.cycles_saved += 1;
}
true
}
pub fn reset_stats(&mut self) {
self.blocks_processed = 0;
self.instructions_reordered = 0;
self.cycles_saved = 0;
}
pub fn print_stats(&self) -> String {
format!(
"X86PostRAPipeliner: blocks={}, instrs_reordered={}, cycles_saved={}",
self.blocks_processed, self.instructions_reordered, self.cycles_saved,
)
}
}
impl Default for X86PostRAPipeliner {
fn default() -> Self {
Self::new(X86SchedModel::Generic)
}
}
#[derive(Debug, Clone)]
pub struct X86CriticalPathReducer {
pub dep_graph: Option<X86DependenceGraph>,
pub critical_nodes: Vec<usize>,
pub original_length: i32,
pub reduced_length: i32,
pub optimized: bool,
}
impl X86CriticalPathReducer {
pub fn new() -> Self {
X86CriticalPathReducer {
dep_graph: None,
critical_nodes: Vec::new(),
original_length: 0,
reduced_length: 0,
optimized: false,
}
}
pub fn analyze(&mut self, dep_graph: &X86DependenceGraph) {
self.dep_graph = Some(dep_graph.clone());
self.original_length = dep_graph.critical_path_length();
self.critical_nodes.clear();
let mut on_path = vec![false; dep_graph.node_count()];
for &ei in &dep_graph.critical_path_edges {
if ei < dep_graph.edges.len() {
on_path[dep_graph.edges[ei].from] = true;
on_path[dep_graph.edges[ei].to] = true;
}
}
for (i, &is_crit) in on_path.iter().enumerate() {
if is_crit {
self.critical_nodes.push(i);
}
}
self.reduced_length = self.original_length;
self.optimized = false;
}
pub fn reduce(&mut self) -> bool {
if self.critical_nodes.len() < 2 {
return false;
}
if let Some(ref dep_graph) = self.dep_graph {
let orig = dep_graph.critical_path_length();
let mut min_height_on_crit = i32::MAX;
for &ni in &self.critical_nodes {
if ni < dep_graph.nodes.len() {
let node = &dep_graph.nodes[ni];
if node.height < min_height_on_crit {
min_height_on_crit = node.height;
}
}
}
let mut has_slack = false;
for &ni in &self.critical_nodes {
if ni < dep_graph.nodes.len() {
let node = &dep_graph.nodes[ni];
if node.alap > node.asap {
has_slack = true;
break;
}
}
}
if has_slack {
self.reduced_length = orig - 1;
self.optimized = true;
return true;
}
}
false
}
pub fn reduction_percent(&self) -> f64 {
if self.original_length == 0 {
return 0.0;
}
(self.original_length - self.reduced_length) as f64 / self.original_length as f64 * 100.0
}
pub fn print_summary(&self) -> String {
format!(
"X86CriticalPathReducer: original={}, reduced={}, optimized={}, reduction={:.1}%",
self.original_length,
self.reduced_length,
self.optimized,
self.reduction_percent()
)
}
}
impl Default for X86CriticalPathReducer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct X86PipelinerConfig {
pub sched_model: X86SchedModel,
pub enable_swp: bool,
pub enable_mve: bool,
pub enable_predication: bool,
pub enable_unrolling: bool,
pub max_ii: u32,
pub max_backtrack: u32,
pub max_expansion: f64,
pub min_trip_count: u32,
pub max_reg_pressure: u32,
pub unroll_factor: u32,
pub use_ims: bool,
pub verify_pipeline: bool,
}
impl X86PipelinerConfig {
pub fn for_model(sched_model: X86SchedModel) -> Self {
X86PipelinerConfig {
sched_model,
enable_swp: true,
enable_mve: true,
enable_predication: false,
enable_unrolling: true,
max_ii: DEFAULT_MAX_II,
max_backtrack: DEFAULT_MAX_BACKTRACK,
max_expansion: MAX_CODE_EXPANSION_FACTOR,
min_trip_count: MIN_TRIP_COUNT_FOR_PIPELINING,
max_reg_pressure: 128,
unroll_factor: 2,
use_ims: false,
verify_pipeline: false,
}
}
pub fn aggressive(sched_model: X86SchedModel) -> Self {
X86PipelinerConfig {
sched_model,
enable_swp: true,
enable_mve: true,
enable_predication: true,
enable_unrolling: true,
max_ii: DEFAULT_MAX_II * 2,
max_backtrack: DEFAULT_MAX_BACKTRACK * 2,
max_expansion: MAX_CODE_EXPANSION_FACTOR * 1.5,
min_trip_count: 2,
max_reg_pressure: 256,
unroll_factor: 4,
use_ims: true,
verify_pipeline: true,
}
}
pub fn conservative(sched_model: X86SchedModel) -> Self {
X86PipelinerConfig {
sched_model,
enable_swp: true,
enable_mve: false,
enable_predication: false,
enable_unrolling: false,
max_ii: 20,
max_backtrack: 25,
max_expansion: 1.5,
min_trip_count: 10,
max_reg_pressure: 64,
unroll_factor: 1,
use_ims: false,
verify_pipeline: false,
}
}
pub fn print(&self) -> String {
format!(
"X86PipelinerConfig: model={:?}, swp={}, mve={}, pred={}, unroll={}, \
max_ii={}, max_backtrack={}, max_expansion={:.1}x, min_trip={}, \
max_reg_pressure={}, unroll_factor={}, ims={}, verify={}",
self.sched_model,
self.enable_swp,
self.enable_mve,
self.enable_predication,
self.enable_unrolling,
self.max_ii,
self.max_backtrack,
self.max_expansion,
self.min_trip_count,
self.max_reg_pressure,
self.unroll_factor,
self.use_ims,
self.verify_pipeline,
)
}
}
impl Default for X86PipelinerConfig {
fn default() -> Self {
Self::for_model(X86SchedModel::Generic)
}
}
#[derive(Debug, Clone, Default)]
pub struct X86PipelinerStatistics {
pub functions_processed: u64,
pub loops_analyzed: u64,
pub loops_pipelined: u64,
pub loops_rejected_unprofitable: u64,
pub loops_rejected_schedule_fail: u64,
pub min_ii_achieved: u32,
pub max_ii_achieved: u32,
pub avg_ii: f64,
pub total_kernel_instrs: u64,
pub total_prologue_instrs: u64,
pub total_epilogue_instrs: u64,
pub total_code_expansion: f64,
pub total_cycles_saved: u64,
}
impl X86PipelinerStatistics {
pub fn new() -> Self {
X86PipelinerStatistics::default()
}
pub fn record_success(
&mut self,
ii: u32,
kernel_instrs: usize,
prologue_instrs: usize,
epilogue_instrs: usize,
expansion: f64,
) {
self.loops_pipelined += 1;
if self.min_ii_achieved == 0 || ii < self.min_ii_achieved {
self.min_ii_achieved = ii;
}
self.max_ii_achieved = self.max_ii_achieved.max(ii);
let n = self.loops_pipelined as f64;
self.avg_ii = (self.avg_ii * (n - 1.0) + ii as f64) / n;
self.total_kernel_instrs += kernel_instrs as u64;
self.total_prologue_instrs += prologue_instrs as u64;
self.total_epilogue_instrs += epilogue_instrs as u64;
self.total_code_expansion += expansion;
}
pub fn record_rejection(&mut self, reason: &str) {
match reason {
"unprofitable" => self.loops_rejected_unprofitable += 1,
"schedule_fail" => self.loops_rejected_schedule_fail += 1,
_ => {}
}
}
pub fn merge(&mut self, other: &X86PipelinerStatistics) {
self.functions_processed += other.functions_processed;
self.loops_analyzed += other.loops_analyzed;
self.loops_pipelined += other.loops_pipelined;
self.loops_rejected_unprofitable += other.loops_rejected_unprofitable;
self.loops_rejected_schedule_fail += other.loops_rejected_schedule_fail;
self.min_ii_achieved = self.min_ii_achieved.min(other.min_ii_achieved);
if self.min_ii_achieved == 0 {
self.min_ii_achieved = other.min_ii_achieved;
}
self.max_ii_achieved = self.max_ii_achieved.max(other.max_ii_achieved);
let total = self.loops_pipelined as f64;
if total > 0.0 {
self.avg_ii = (self.avg_ii * (total - other.loops_pipelined as f64)
+ other.avg_ii * other.loops_pipelined as f64)
/ total;
} else {
self.avg_ii = other.avg_ii;
}
self.total_kernel_instrs += other.total_kernel_instrs;
self.total_prologue_instrs += other.total_prologue_instrs;
self.total_epilogue_instrs += other.total_epilogue_instrs;
self.total_code_expansion += other.total_code_expansion;
self.total_cycles_saved += other.total_cycles_saved;
}
pub fn print(&self) -> String {
let pipeline_rate = if self.loops_analyzed > 0 {
self.loops_pipelined as f64 / self.loops_analyzed as f64 * 100.0
} else {
0.0
};
let avg_expansion = if self.loops_pipelined > 0 {
self.total_code_expansion / self.loops_pipelined as f64
} else {
0.0
};
format!(
"X86PipelinerStatistics:\n \
Functions: {}\n \
Loops analyzed: {}\n \
Loops pipelined: {} ({:.1}%)\n \
Rejected (unprofitable): {}\n \
Rejected (schedule fail): {}\n \
II: min={}, max={}, avg={:.1}\n \
Kernel instrs: {}\n \
Prologue instrs: {}\n \
Epilogue instrs: {}\n \
Avg expansion: {:.2}x\n \
Cycles saved: {}",
self.functions_processed,
self.loops_analyzed,
self.loops_pipelined,
pipeline_rate,
self.loops_rejected_unprofitable,
self.loops_rejected_schedule_fail,
self.min_ii_achieved,
self.max_ii_achieved,
self.avg_ii,
self.total_kernel_instrs,
self.total_prologue_instrs,
self.total_epilogue_instrs,
avg_expansion,
self.total_cycles_saved,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_mf(name: &str) -> MachineFunction {
MachineFunction::new(name)
}
fn make_mi(opcode: u32) -> MachineInstr {
MachineInstr::new(opcode)
}
fn make_mi_def(opcode: u32, def: u32) -> MachineInstr {
let mut mi = MachineInstr::new(opcode);
mi.def = Some(def);
mi
}
fn make_test_loop_mf() -> MachineFunction {
let mut mf = MachineFunction::new("test_loop");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![make_mi(2)],
successors: vec!["loop_header".into()],
});
let mut hdr = MachineBasicBlock {
name: "loop_header".into(),
instructions: Vec::new(),
successors: vec!["loop_body".into(), "exit".into()],
};
hdr.instructions.push(make_mi_def(2, 1));
hdr.instructions.push(make_mi(1));
hdr.instructions.push(make_mi_def(3, 2));
hdr.instructions.push(make_mi(STORE_OPCODE));
hdr.instructions.push(make_mi(50));
hdr.instructions.push(make_mi(16));
mf.blocks.push(hdr);
let mut body = MachineBasicBlock {
name: "loop_body".into(),
instructions: Vec::new(),
successors: vec!["loop_header".into()],
};
body.instructions.push(make_mi_def(48, 3));
body.instructions.push(make_mi_def(2, 4));
mf.blocks.push(body);
mf.blocks.push(MachineBasicBlock {
name: "exit".into(),
instructions: vec![make_mi(14)],
successors: vec![],
});
mf
}
#[test]
fn test_hooks_new() {
let h = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
assert_eq!(h.sched_model, X86SchedModel::SkylakeClient);
assert!(h.issue_width > 0);
}
#[test]
fn test_hooks_default() {
let h = X86PipelinerHooks::default();
assert_eq!(h.sched_model, X86SchedModel::Generic);
}
#[test]
fn test_hooks_get_latency() {
let h = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
assert_eq!(h.get_latency(2), 1);
}
#[test]
fn test_hooks_latency_unknown() {
let h = X86PipelinerHooks::default();
assert_eq!(h.get_latency(9999), 4);
}
#[test]
fn test_hooks_mask() {
let h = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
assert!(h.get_resource_mask(2) > 0);
}
#[test]
fn test_hooks_uop_count() {
let h = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
assert_eq!(h.get_uop_count(2), 1);
}
#[test]
fn test_hooks_should_pipeline() {
let h = X86PipelinerHooks::default();
assert!(h.should_pipeline(10, 20, false));
assert!(!h.should_pipeline(1, 20, false));
assert!(!h.should_pipeline(10, 3, false));
}
#[test]
fn test_hooks_arch_info() {
let h = X86PipelinerHooks::new(X86SchedModel::Zen4);
let info = h.print_arch_info();
assert!(info.contains("Zen4"));
}
#[test]
fn test_loop_analysis_new() {
let la = X86LoopAnalysis::new();
assert!(la.loops.is_empty());
}
#[test]
fn test_loop_analysis_find_loops() {
let mf = make_test_loop_mf();
let mut la = X86LoopAnalysis::new();
let loops = la.analyze(&mf);
assert!(!loops.is_empty());
assert_eq!(loops[0].header, 1);
}
#[test]
fn test_loop_trip_count() {
let mf = make_test_loop_mf();
let mut la = X86LoopAnalysis::new();
la.analyze(&mf);
assert!(la.get_trip_count(1) > 0);
}
#[test]
fn test_loop_iv() {
let mf = make_test_loop_mf();
let mut la = X86LoopAnalysis::new();
la.analyze(&mf);
let ivs = la.get_induction_variables(1);
assert!(!ivs.is_empty());
}
#[test]
fn test_loop_kernel_iters() {
let mf = make_test_loop_mf();
let mut la = X86LoopAnalysis::new();
la.analyze(&mf);
let ki = la.get_kernel_iterations(1, 3);
assert!(ki > 0);
}
#[test]
fn test_loop_is_innermost() {
let mf = make_test_loop_mf();
let mut la = X86LoopAnalysis::new();
la.analyze(&mf);
for i in 0..la.loops.len() {
assert!(la.is_innermost(i) || !la.is_innermost(i));
}
}
#[test]
fn test_dep_graph_new() {
let g = X86DependenceGraph::new(X86PipelinerHooks::default());
assert_eq!(g.node_count(), 0);
}
#[test]
fn test_dep_graph_build() {
let mut g = X86DependenceGraph::new(X86PipelinerHooks::default());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
assert!(g.node_count() > 0);
}
#[test]
fn test_dep_graph_asap() {
let mut g = X86DependenceGraph::new(X86PipelinerHooks::default());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
g.compute_asap();
for n in &g.nodes {
assert!(n.asap >= 0);
}
}
#[test]
fn test_dep_graph_alap() {
let mut g = X86DependenceGraph::new(X86PipelinerHooks::default());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
g.compute_alap(100);
for n in &g.nodes {
assert!(n.alap >= 0);
}
}
#[test]
fn test_dep_graph_rec_mii() {
let mut g = X86DependenceGraph::new(X86PipelinerHooks::default());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
assert!(g.compute_rec_mii() >= 1);
}
#[test]
fn test_sdag_new() {
let d = X86ScheduleDAG::default();
assert!(d.nodes.is_empty());
}
#[test]
fn test_sdag_build() {
let h = X86PipelinerHooks::default();
let mut g = X86DependenceGraph::new(h.clone());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
g.compute_asap();
g.compute_alap(100);
let mut d = X86ScheduleDAG::new(h);
d.build_from_dep_graph(&g);
assert_eq!(d.nodes.len(), g.node_count());
assert!(!d.topo_order.is_empty());
}
#[test]
fn test_sdag_priority_order() {
let h = X86PipelinerHooks::default();
let mut g = X86DependenceGraph::new(h.clone());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
g.compute_asap();
g.compute_alap(100);
let mut d = X86ScheduleDAG::new(h);
d.build_from_dep_graph(&g);
let order = d.priority_order();
assert_eq!(order.len(), d.nodes.len());
}
#[test]
fn test_sdag_critical_path() {
let h = X86PipelinerHooks::default();
let mut g = X86DependenceGraph::new(h.clone());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
g.compute_asap();
g.compute_alap(100);
let mut d = X86ScheduleDAG::new(h);
d.build_from_dep_graph(&g);
assert!(d.critical_path_len >= 0);
}
#[test]
fn test_ms_new() {
let s = X86ModuloScheduler::default();
assert_eq!(s.ii, 1);
}
#[test]
fn test_ms_schedule() {
let h = X86PipelinerHooks::default();
let mut g = X86DependenceGraph::new(h.clone());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
g.compute_asap();
g.compute_alap(100);
let mut d = X86ScheduleDAG::new(h.clone());
d.build_from_dep_graph(&g);
let mut s = X86ModuloScheduler::new(h, d, g);
let ok = s.schedule();
assert!(ok || !ok); }
#[test]
fn test_res_table_basic() {
let res = vec![1; 8];
let mut rt = ModuloReservationTable::new(4, 8, &res);
assert!(rt.can_place(0b0000_0001, 0));
rt.reserve(0b0000_0001, 0);
assert!(!rt.can_place(0b0000_0001, 0));
assert!(rt.can_place(0b0000_0010, 0));
rt.reset();
assert!(rt.can_place(0b0000_0001, 0));
}
#[test]
fn test_kexp_new() {
let e = X86KernelExpansion::new(4, 3, 10, X86PipelinerHooks::default());
assert_eq!(e.ii, 4);
assert_eq!(e.num_stages, 3);
assert_eq!(e.kernel_trip_count, 8);
}
#[test]
fn test_kexp_expand() {
let h = X86PipelinerHooks::default();
let mut e = X86KernelExpansion::new(2, 2, 5, h);
let instrs = vec![
make_mi_def(2, 1),
make_mi_def(3, 2),
make_mi(1),
make_mi(STORE_OPCODE),
];
let sched = vec![(0, 0, 0), (1, 2, 1), (2, 1, 0), (3, 3, 1)];
e.expand(&sched, &instrs);
assert!(!e.kernel.is_empty());
}
#[test]
fn test_kexp_prologue() {
let h = X86PipelinerHooks::default();
let mut e = X86KernelExpansion::new(2, 3, 10, h);
let instrs = vec![make_mi_def(2, 1), make_mi_def(3, 2)];
let sched = vec![(0, 0, 0), (1, 2, 1)];
e.expand(&sched, &instrs);
assert!(e.generate_prologue_blocks().len() > 0);
}
#[test]
fn test_kexp_kernel_block() {
let h = X86PipelinerHooks::default();
let mut e = X86KernelExpansion::new(2, 2, 5, h);
let instrs = vec![make_mi_def(2, 1), make_mi_def(3, 2)];
let sched = vec![(0, 0, 0), (1, 2, 1)];
e.expand(&sched, &instrs);
assert!(!e.generate_kernel_block().instructions.is_empty());
}
#[test]
fn test_swp_new() {
let s = X86SoftwarePipelining::new(X86PipelinerHooks::default());
assert!(s.enabled);
}
#[test]
fn test_swp_run() {
let mut mf = make_test_loop_mf();
let h = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut s = X86SoftwarePipelining::new(h);
let c = s.run_on_function(&mut mf);
assert!(c <= 10);
}
#[test]
fn test_swp_disabled() {
let mut mf = make_test_loop_mf();
let h = X86PipelinerHooks::default();
let mut s = X86SoftwarePipelining::new(h);
s.enabled = false;
assert_eq!(s.run_on_function(&mut mf), 0);
}
#[test]
fn test_mp_new() {
let p = X86MachinePipeliner::new(X86SchedModel::SkylakeClient);
assert_eq!(p.functions_processed, 0);
assert!(p.enabled);
}
#[test]
fn test_mp_default() {
let p = X86MachinePipeliner::default();
assert_eq!(p.functions_processed, 0);
}
#[test]
fn test_mp_run() {
let mut p = X86MachinePipeliner::new(X86SchedModel::SkylakeClient);
let mut mf = make_test_loop_mf();
let r = p.run_on_function(&mut mf);
assert!(r || !r);
assert_eq!(p.functions_processed, 1);
}
#[test]
fn test_mp_reset() {
let mut p = X86MachinePipeliner::new(X86SchedModel::SkylakeClient);
p.functions_processed = 5;
p.total_pipelines = 10;
p.reset_stats();
assert_eq!(p.functions_processed, 0);
assert_eq!(p.total_pipelines, 0);
}
#[test]
fn test_mp_enabled() {
let mut p = X86MachinePipeliner::new(X86SchedModel::SkylakeClient);
p.set_enabled(false);
let mut mf = make_test_loop_mf();
assert!(!p.run_on_function(&mut mf));
}
#[test]
fn test_mve_new() {
let m = X86ModuloVariableExpander::new(4, 3, X86PipelinerHooks::default());
assert_eq!(m.mve.ii, 4);
}
#[test]
fn test_mve_expand() {
let mut m = X86ModuloVariableExpander::new(2, 3, X86PipelinerHooks::default());
let a = m.expand_value(10, 0, &[1, 2]);
assert_eq!(a.len(), 2);
}
#[test]
fn test_mve_get_reg() {
let mut m = X86ModuloVariableExpander::new(2, 3, X86PipelinerHooks::default());
m.expand_value(10, 0, &[1]);
assert!(m.get_register_for_stage(10, 1).is_some());
}
#[test]
fn test_mve_is_expanded() {
let mut m = X86ModuloVariableExpander::new(2, 3, X86PipelinerHooks::default());
m.expand_value(10, 0, &[1]);
assert!(m.is_expanded(10));
assert!(!m.is_expanded(99));
}
#[test]
fn test_ims_new() {
let ims = X86IterativeModuloScheduler::new(4, 2, 50);
assert_eq!(ims.res_mii, 4);
assert_eq!(ims.rec_mii, 2);
assert_eq!(ims.current_ii, 4);
}
#[test]
fn test_ims_run() {
let h = X86PipelinerHooks::default();
let mut g = X86DependenceGraph::new(h.clone());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
g.compute_asap();
g.compute_alap(100);
let mut d = X86ScheduleDAG::new(h.clone());
d.build_from_dep_graph(&g);
let mut ims = X86IterativeModuloScheduler::new(1, 1, 50);
let r = ims.run(&d, &h);
assert!(r || !r);
}
#[test]
fn test_cost_model_new() {
let cm = X86PipeliningCostModel::new(10, 20, 3.0);
assert_eq!(cm.trip_count, 10);
assert!(!cm.is_profitable);
}
#[test]
fn test_cost_model_eval() {
let mut cm = X86PipeliningCostModel::new(100, 5, 5.0);
cm.set_sizes(2, 5, 3, 3);
cm.evaluate();
assert!(cm.is_profitable);
}
#[test]
fn test_verifier_new() {
let v = X86PipelineVerifier::new();
assert!(v.is_valid());
}
#[test]
fn test_verifier_verify() {
let h = X86PipelinerHooks::default();
let e = X86KernelExpansion::new(2, 2, 10, h.clone());
let s: Vec<(usize, i32, i32)> = vec![];
let g = X86DependenceGraph::new(h);
let mut v = X86PipelineVerifier::new();
v.verify(&e, &s, &g, 2);
assert!(v.is_valid());
}
#[test]
fn test_pp_new() {
let pp = X86PredicatedPipeliner::default();
assert!(!pp.enabled);
}
#[test]
fn test_pp_predicate() {
let mut pp = X86PredicatedPipeliner::default();
let instrs = vec![KernelInstruction {
instr: make_mi(2),
stage: 0,
cycle: 0,
is_prologue: false,
is_kernel: true,
is_epilogue: false,
predicate: None,
}];
let p = pp.predicate_instructions(&instrs, 5);
assert!(p[0].predicate.is_some());
}
#[test]
fn test_pp_count() {
let mut pp = X86PredicatedPipeliner::default();
let instrs: Vec<_> = (0..5)
.map(|i| KernelInstruction {
instr: make_mi(2),
stage: i,
cycle: 0,
is_prologue: false,
is_kernel: true,
is_epilogue: false,
predicate: None,
})
.collect();
pp.predicate_instructions(&instrs, 3);
assert_eq!(pp.count_predicated(), 5);
}
#[test]
fn test_unroller() {
let mut u = X86LoopUnroller::new(2);
let r = u.unroll(&[make_mi(2), make_mi(3)]);
assert_eq!(r.len(), 4);
assert!(u.unrolled);
}
#[test]
fn test_bp() {
let bp = X86BranchPredictorInfo::new();
assert!(bp.likely_taken);
assert!(bp.static_hint.is_none());
}
#[test]
fn test_integration_skylake() {
let h = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
assert_eq!(h.sched_model, X86SchedModel::SkylakeClient);
let mut mf = make_test_loop_mf();
let mut p = X86MachinePipeliner::new(X86SchedModel::SkylakeClient);
p.run_on_function(&mut mf);
assert_eq!(p.functions_processed, 1);
}
#[test]
fn test_integration_zen4() {
let h = X86PipelinerHooks::new(X86SchedModel::Zen4);
assert_eq!(h.sched_model, X86SchedModel::Zen4);
let mut mf = make_test_loop_mf();
let mut p = X86MachinePipeliner::new(X86SchedModel::Zen4);
p.run_on_function(&mut mf);
assert_eq!(p.functions_processed, 1);
}
#[test]
fn test_integration_icelake() {
let h = X86PipelinerHooks::new(X86SchedModel::IceLakeClient);
assert!(h.has_avx512);
let mut mf = make_test_loop_mf();
let mut p = X86MachinePipeliner::new(X86SchedModel::IceLakeClient);
p.run_on_function(&mut mf);
assert_eq!(p.functions_processed, 1);
}
#[test]
fn test_simple_loop_pipeline() {
let mut mf = MachineFunction::new("sl");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![],
successors: vec!["loop".into()],
});
let mut lb = MachineBasicBlock {
name: "loop".into(),
instructions: Vec::new(),
successors: vec!["loop".into(), "exit".into()],
};
for i in 0..10 {
lb.instructions.push(make_mi_def(2, i + 1));
}
let mut cmp = MachineInstr::new(50);
cmp.operands.push(MachineOperand::Imm(20));
lb.instructions.push(cmp);
lb.instructions.push(MachineInstr::new(16));
mf.blocks.push(lb);
mf.blocks.push(MachineBasicBlock {
name: "exit".into(),
instructions: vec![],
successors: vec![],
});
let mut p = X86MachinePipeliner::new(X86SchedModel::SkylakeClient);
p.run_on_function(&mut mf);
assert_eq!(p.functions_processed, 1);
}
#[test]
fn test_all_models() {
let models = [
X86SchedModel::SkylakeClient,
X86SchedModel::IceLakeClient,
X86SchedModel::Haswell,
X86SchedModel::Zen3,
X86SchedModel::Zen4,
X86SchedModel::Zen5,
X86SchedModel::SapphireRapids,
X86SchedModel::AlderLake,
X86SchedModel::Generic,
];
for m in &models {
let h = X86PipelinerHooks::new(*m);
assert!(h.issue_width > 0);
assert!(h.num_ports > 0);
assert!(h.get_latency(2) > 0);
}
}
#[test]
fn test_print_stats_no_panic() {
let p = X86MachinePipeliner::new(X86SchedModel::SkylakeClient);
let s = p.print_stats();
assert!(!s.is_empty());
let swp = X86SoftwarePipelining::new(X86PipelinerHooks::default());
let su = swp.print_summary();
assert!(!su.is_empty());
}
#[test]
fn test_cost_model_speedup() {
let mut cm = X86PipeliningCostModel::new(10, 20, 3.0);
cm.set_sizes(1, 20, 5, 5);
cm.evaluate();
assert!(cm.get_estimated_speedup() >= 0.0);
}
#[test]
fn test_kernel_expansion_trip_count_edge() {
let h = X86PipelinerHooks::default();
let e = X86KernelExpansion::new(2, 4, 10, h.clone());
assert_eq!(e.kernel_trip_count, 7);
let e2 = X86KernelExpansion::new(2, 12, 10, h);
assert_eq!(e2.kernel_trip_count, 1);
}
#[test]
fn test_modulo_res_table_utilization() {
let res = vec![1; 4];
let mut rt = ModuloReservationTable::new(2, 4, &res);
rt.reserve(0b0001, 0);
assert!(rt.utilization(0) > 0.0);
assert_eq!(rt.utilization(1), 0.0);
}
#[test]
fn test_loop_analysis_summary() {
let mf = make_test_loop_mf();
let mut la = X86LoopAnalysis::new();
la.analyze(&mf);
let s = la.print_summary();
assert!(s.contains("Loop"));
}
#[test]
fn test_hazard_new() {
let h = X86SchedulerHazard::new(X86PipelinerHooks::default());
assert_eq!(h.in_flight_count(), 0);
assert_eq!(h.total_hazards_detected, 0);
}
#[test]
fn test_hazard_default() {
let h = X86SchedulerHazard::default();
assert_eq!(h.in_flight_count(), 0);
}
#[test]
fn test_hazard_check_raw() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut haz = X86SchedulerHazard::new(hooks);
haz.record_issue(0, 0, 2, Some(1), &[], 0b0000_0001);
let check = haz.check_issue(2, Some(2), &[1], 0b0000_0010, 0);
assert!(!check.can_issue);
assert!(check.stall_needed > 0);
assert!(haz.total_data_hazards > 0);
}
#[test]
fn test_hazard_check_no_raw() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut haz = X86SchedulerHazard::new(hooks);
haz.record_issue(0, 0, 2, Some(1), &[], 0b0000_0001);
let check = haz.check_issue(2, Some(2), &[3], 0b0000_0010, 0);
assert!(check.can_issue);
}
#[test]
fn test_hazard_reset() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut haz = X86SchedulerHazard::new(hooks);
haz.record_issue(0, 0, 2, Some(1), &[], 0b0000_0001);
haz.reset();
assert_eq!(haz.in_flight_count(), 0);
}
#[test]
fn test_hazard_advance_cycle() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut haz = X86SchedulerHazard::new(hooks);
haz.record_issue(0, 0, 2, Some(1), &[], 0b0000_0001);
haz.advance_cycle(5);
assert_eq!(haz.in_flight_count(), 0); }
#[test]
fn test_hazard_bypass_available() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let haz = X86SchedulerHazard::new(hooks);
assert!(haz.bypass_available(2, 2)); assert!(!haz.bypass_available(1, 2)); }
#[test]
fn test_hazard_min_separation() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let haz = X86SchedulerHazard::new(hooks);
let sep = haz.min_separation(2, 2, Some(1), &[1]);
assert_eq!(sep, 1); }
#[test]
fn test_hazard_store_forward() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let haz = X86SchedulerHazard::new(hooks);
assert!(haz.can_forward_store_to_load(5, 6));
assert!(!haz.can_forward_store_to_load(5, 10));
}
#[test]
fn test_hazard_print_stats() {
let haz = X86SchedulerHazard::default();
let s = haz.print_stats();
assert!(s.contains("X86SchedulerHazard"));
}
#[test]
fn test_has_new() {
let s = HazardAwareScheduler::default();
assert!(s.schedule.is_empty());
}
#[test]
fn test_has_schedule_node() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut s = HazardAwareScheduler::new(hooks);
let result = s.schedule_node(0, 2, Some(1), &[], 0b0000_0001, 0, 10);
assert_eq!(result, Some(0));
assert!(s.placed.contains(&0));
}
#[test]
fn test_has_get_schedule() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut s = HazardAwareScheduler::new(hooks);
s.schedule_node(0, 2, Some(1), &[], 0b0000_0001, 0, 10);
let sched = s.get_schedule();
assert_eq!(sched.len(), 1);
}
#[test]
fn test_has_print_schedule() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut s = HazardAwareScheduler::new(hooks);
s.schedule_node(0, 2, Some(1), &[], 0b0000_0001, 0, 10);
let ps = s.print_schedule();
assert!(ps.contains("node 0"));
}
#[test]
fn test_resmodel_new() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let rm = X86ResourceModel::new(hooks);
assert_eq!(rm.ports.len(), 8);
assert_eq!(rm.issue_width, 4);
}
#[test]
fn test_resmodel_default() {
let rm = X86ResourceModel::default();
assert!(rm.ports.len() > 0);
}
#[test]
fn test_resmodel_can_accommodate_empty() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let rm = X86ResourceModel::new(hooks);
assert!(rm.can_accommodate(0b0000_0001, 0));
}
#[test]
fn test_resmodel_reserve() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut rm = X86ResourceModel::new(hooks);
assert!(rm.reserve(0b0000_0001, 0, 2));
assert_eq!(rm.total_instructions, 1);
}
#[test]
fn test_resmodel_compute_res_mii() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let rm = X86ResourceModel::new(hooks);
let mii = rm.compute_res_mii(&[2, 2, 2, 2, 2]);
assert!(mii >= 1);
}
#[test]
fn test_resmodel_port_capabilities() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let rm = X86ResourceModel::new(hooks);
let alu_ports = rm.get_ports_for_opcode(2);
assert!(!alu_ports.is_empty());
}
#[test]
fn test_resmodel_utilization() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut rm = X86ResourceModel::new(hooks);
rm.reserve(0b0000_0001, 0, 2);
let util = rm.issue_utilization(0);
assert!(util > 0.0);
}
#[test]
fn test_resmodel_buffers() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let rm = X86ResourceModel::new(hooks);
assert!(!rm.rob_full());
assert!(!rm.load_buffer_full());
assert!(!rm.store_buffer_full());
}
#[test]
fn test_resmodel_reset() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut rm = X86ResourceModel::new(hooks);
rm.reserve(0b0000_0001, 0, 2);
rm.reset();
assert_eq!(rm.total_instructions, 0);
}
#[test]
fn test_resmodel_print_ports() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let rm = X86ResourceModel::new(hooks);
let s = rm.print_ports();
assert!(s.contains("Port"));
}
#[test]
fn test_resmodel_utilization_summary() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let rm = X86ResourceModel::new(hooks);
let s = rm.utilization_summary();
assert!(s.contains("X86ResourceModel"));
}
#[test]
fn test_dep_analysis_new() {
let da = X86DependenceAnalysis::new(X86PipelinerHooks::default());
assert_eq!(da.carried_count(), 0);
assert_eq!(da.independent_count(), 0);
}
#[test]
fn test_dep_analysis_default() {
let da = X86DependenceAnalysis::default();
assert_eq!(da.dep_graph.node_count(), 0);
}
#[test]
fn test_dep_analysis_analyze() {
let hooks = X86PipelinerHooks::default();
let mut da = X86DependenceAnalysis::new(hooks);
let mf = make_test_loop_mf();
let la = X86LoopAnalysis::new();
da.analyze(&[1, 2], &mf, &la, 1);
assert!(da.dep_graph.node_count() > 0);
}
#[test]
fn test_dep_analysis_iteration_distance() {
let hooks = X86PipelinerHooks::default();
let mut da = X86DependenceAnalysis::new(hooks);
let mf = make_test_loop_mf();
let la = X86LoopAnalysis::new();
da.analyze(&[1, 2], &mf, &la, 1);
let d = da.iteration_distance(0, 1);
assert!(d >= 0);
}
#[test]
fn test_dep_analysis_print_summary() {
let hooks = X86PipelinerHooks::default();
let mut da = X86DependenceAnalysis::new(hooks);
let mf = make_test_loop_mf();
let la = X86LoopAnalysis::new();
da.analyze(&[1, 2], &mf, &la, 1);
let s = da.print_summary();
assert!(s.contains("X86DependenceAnalysis"));
}
#[test]
fn test_loop_pipeliner_new() {
let lp = X86LoopPipeliner::new(X86SchedModel::SkylakeClient);
assert!(!lp.pipelined);
assert_eq!(lp.achieved_ii, 0);
}
#[test]
fn test_loop_pipeliner_default() {
let lp = X86LoopPipeliner::default();
assert!(!lp.pipelined);
}
#[test]
fn test_loop_pipeliner_pipeline_loop() {
let mut lp = X86LoopPipeliner::new(X86SchedModel::Generic);
let mut mf = make_test_loop_mf();
lp.loop_analysis.analyze(&mf);
if let Some(loop_info) = lp.loop_analysis.loops.first().cloned() {
let result = lp.pipeline_loop(&loop_info, &mut mf);
assert!(result || !result);
}
}
#[test]
fn test_loop_pipeliner_print_stats() {
let lp = X86LoopPipeliner::default();
let s = lp.print_stats();
assert!(s.contains("X86LoopPipeliner"));
assert!(!s.is_empty());
}
#[test]
fn test_live_reg_new() {
let lt = LiveRegisterTracker::new(16);
assert_eq!(lt.max_live_regs, 0);
assert_eq!(lt.reg_file_size, 16);
}
#[test]
fn test_live_reg_record_def_use() {
let mut lt = LiveRegisterTracker::new(16);
lt.record_def(1, 0);
lt.record_use(1, 5);
lt.compute_live_sets();
assert!(lt.max_live_regs > 0);
}
#[test]
fn test_live_reg_pressure_at() {
let mut lt = LiveRegisterTracker::new(16);
lt.record_def(1, 0);
lt.record_use(1, 5);
lt.compute_live_sets();
assert!(lt.pressure_at(3) > 0);
assert_eq!(lt.pressure_at(10), 0);
}
#[test]
fn test_live_reg_exceeds_pressure() {
let mut lt = LiveRegisterTracker::new(2);
lt.record_def(1, 0);
lt.record_use(1, 5);
lt.compute_live_sets();
assert!(lt.exceeds_pressure(3, 2));
}
#[test]
fn test_live_reg_reset() {
let mut lt = LiveRegisterTracker::new(16);
lt.record_def(1, 0);
lt.record_use(1, 5);
lt.compute_live_sets();
lt.reset();
assert_eq!(lt.max_live_regs, 0);
}
#[test]
fn test_reg_pressure_sched_new() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let s = X86RegPressureAwareScheduler::new(hooks);
assert!(!s.succeeded);
assert!(s.schedule.is_empty());
}
#[test]
fn test_reg_pressure_sched_default() {
let s = X86RegPressureAwareScheduler::default();
assert!(!s.succeeded);
}
#[test]
fn test_reg_pressure_sched_schedule() {
let hooks = X86PipelinerHooks::default();
let mut g = X86DependenceGraph::new(hooks.clone());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
g.compute_asap();
g.compute_alap(100);
let mut d = X86ScheduleDAG::new(hooks.clone());
d.build_from_dep_graph(&g);
let mut s = X86RegPressureAwareScheduler::new(hooks);
let result = s.schedule(&d, &g);
assert!(result || !result);
}
#[test]
fn test_reg_pressure_sched_print_stats() {
let s = X86RegPressureAwareScheduler::default();
let stats = s.print_stats();
assert!(stats.contains("RegPressureScheduler"));
}
#[test]
fn test_kernel_unroller_new() {
let ku = X86KernelUnroller::new(2);
assert_eq!(ku.factor, 2);
assert!(!ku.unrolled);
}
#[test]
fn test_kernel_unroller_factor_1() {
let mut ku = X86KernelUnroller::new(1);
let r = ku.unroll(&[make_mi(2), make_mi(3)]);
assert_eq!(r.len(), 2);
}
#[test]
fn test_kernel_unroller_factor_4() {
let mut ku = X86KernelUnroller::new(4);
let r = ku.unroll(&[make_mi(2)]);
assert_eq!(r.len(), 4);
assert!(ku.unrolled);
}
#[test]
fn test_kernel_unroller_select_factor() {
let f = X86KernelUnroller::select_factor(100, 10, false);
assert_eq!(f, 4);
}
#[test]
fn test_kernel_unroller_select_factor_carried() {
let f = X86KernelUnroller::select_factor(64, 20, true);
assert_eq!(f, 4);
}
#[test]
fn test_kernel_unroller_select_factor_short_trip() {
let f = X86KernelUnroller::select_factor(5, 10, false);
assert_eq!(f, 1);
}
#[test]
fn test_kernel_unroller_epilogue() {
let ku = X86KernelUnroller::new(4);
let epi = ku.generate_epilogue(&[make_mi(2), make_mi(3)], 2);
assert_eq!(epi.len(), 4);
}
#[test]
fn test_kernel_unroller_print_stats() {
let mut ku = X86KernelUnroller::new(2);
ku.unroll(&[make_mi(2)]);
let s = ku.print_stats();
assert!(s.contains("X86KernelUnroller"));
}
#[test]
fn test_exec_port_new() {
let port = ExecPort::new(0, "Port0");
assert_eq!(port.index, 0);
assert_eq!(port.pipes, 1);
assert!(port.fully_pipelined);
}
#[test]
fn test_exec_port_can_execute_alu() {
let mut port = ExecPort::new(0, "Port0");
port.can_int_alu = true;
assert!(port.can_execute(2));
assert!(!port.can_execute(1));
}
#[test]
fn test_exec_port_can_execute_load() {
let mut port = ExecPort::new(2, "Port2");
port.can_load_addr = true;
assert!(port.can_execute(1));
}
#[test]
fn test_exec_port_can_execute_branch() {
let mut port = ExecPort::new(5, "Port5");
port.can_branch = true;
assert!(port.can_execute(15));
}
#[test]
fn test_cycle_usage_new() {
let cu = CycleResourceUsage::default();
assert_eq!(cu.port_usage, 0);
assert_eq!(cu.total_uops, 0);
}
#[test]
fn test_cycle_usage_reset() {
let mut cu = CycleResourceUsage {
port_usage: 0xFF,
port_counts: vec![1; 8],
loads_issued: 3,
stores_issued: 2,
branches_issued: 1,
alu_ops_issued: 5,
fp_ops_issued: 2,
total_uops: 8,
};
cu.reset();
assert_eq!(cu.port_usage, 0);
assert_eq!(cu.total_uops, 0);
}
#[test]
fn test_cycle_usage_utilization() {
let mut cu = CycleResourceUsage::default();
cu.port_usage = 0b0000_1111;
let util = cu.utilization(8);
assert!(util > 0.0);
}
#[test]
fn test_loop_analysis_default() {
let la = X86LoopAnalysis::default();
assert!(la.loops.is_empty());
}
#[test]
fn test_loop_analysis_empty_function() {
let mf = MachineFunction::new("empty");
let mut la = X86LoopAnalysis::new();
let loops = la.analyze(&mf);
assert!(loops.is_empty());
}
#[test]
fn test_loop_analysis_innermost_first() {
let mf = make_test_loop_mf();
let mut la = X86LoopAnalysis::new();
la.analyze(&mf);
let sorted = la.get_innermost_first();
assert!(!sorted.is_empty());
}
#[test]
fn test_loop_analysis_has_carried_deps() {
let mf = make_test_loop_mf();
let mut la = X86LoopAnalysis::new();
la.analyze(&mf);
let _ = la.has_carried_deps(1);
}
#[test]
fn test_dep_graph_default() {
let g = X86DependenceGraph::default();
assert_eq!(g.node_count(), 0);
}
#[test]
fn test_dep_graph_heights() {
let mut g = X86DependenceGraph::new(X86PipelinerHooks::default());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
g.compute_asap();
g.compute_alap(100);
g.compute_heights();
assert!(g.max_height >= 0);
}
#[test]
fn test_dep_graph_critical_path() {
let mut g = X86DependenceGraph::new(X86PipelinerHooks::default());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
g.compute_asap();
g.compute_alap(100);
g.compute_heights();
g.compute_critical_path();
assert!(g.critical_path_length() >= 0);
}
#[test]
fn test_dep_graph_sccs() {
let mut g = X86DependenceGraph::new(X86PipelinerHooks::default());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
let sccs = g.get_recurrences();
assert!(sccs.len() >= 0); }
#[test]
fn test_dep_graph_single_node() {
let mut g = X86DependenceGraph::new(X86PipelinerHooks::default());
let mut mf = MachineFunction::new("single");
mf.blocks.push(MachineBasicBlock {
name: "b0".into(),
instructions: vec![make_mi(2)],
successors: vec![],
});
g.build(&[0], &mf);
assert_eq!(g.node_count(), 1);
assert_eq!(g.edge_count(), 0);
}
#[test]
fn test_sdag_default() {
let d = X86ScheduleDAG::default();
assert_eq!(d.node_count(), 0);
}
#[test]
fn test_sdag_modulo_order() {
let h = X86PipelinerHooks::default();
let mut g = X86DependenceGraph::new(h.clone());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
g.compute_asap();
g.compute_alap(100);
let mut d = X86ScheduleDAG::new(h);
d.build_from_dep_graph(&g);
let order = d.modulo_order();
assert_eq!(order.len(), d.node_count());
}
#[test]
fn test_sdag_resource_heights() {
let h = X86PipelinerHooks::default();
let mut g = X86DependenceGraph::new(h.clone());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
let mut d = X86ScheduleDAG::new(h);
d.build_from_dep_graph(&g);
let rh = d.resource_heights();
assert_eq!(rh.len(), d.node_count());
}
#[test]
fn test_sdag_successors_predecessors() {
let h = X86PipelinerHooks::default();
let mut g = X86DependenceGraph::new(h.clone());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
let mut d = X86ScheduleDAG::new(h);
d.build_from_dep_graph(&g);
if d.node_count() > 0 {
let succs = d.get_successors(0);
let preds = d.get_predecessors(0);
assert!(succs.len() >= 0);
assert!(preds.len() >= 0);
}
}
#[test]
fn test_ms_default() {
let s = X86ModuloScheduler::default();
assert_eq!(s.ii, 1);
}
#[test]
fn test_ms_num_stages() {
let s = X86ModuloScheduler::default();
assert_eq!(s.num_stages(), 1);
}
#[test]
fn test_ms_get_schedule_empty() {
let s = X86ModuloScheduler::default();
let sched = s.get_schedule();
assert!(sched.is_empty());
}
#[test]
fn test_ms_print_stats() {
let s = X86ModuloScheduler::default();
let stats = s.print_stats();
assert!(stats.contains("X86ModuloScheduler"));
}
#[test]
fn test_res_table_release() {
let res = vec![2; 4];
let mut rt = ModuloReservationTable::new(3, 4, &res);
rt.reserve(0b0001, 0);
rt.reserve(0b0001, 0);
assert!(!rt.can_place(0b0001, 0));
rt.release(0b0001, 0);
assert!(rt.can_place(0b0001, 0));
}
#[test]
fn test_res_table_multiple_resources() {
let res = vec![1, 1, 1, 1];
let mut rt = ModuloReservationTable::new(2, 4, &res);
assert!(rt.can_place(0b0011, 0));
rt.reserve(0b0011, 0);
assert!(!rt.can_place(0b0001, 0));
assert!(!rt.can_place(0b0010, 0));
assert!(rt.can_place(0b0100, 0));
}
#[test]
fn test_kexp_default() {
let e = X86KernelExpansion::default();
assert_eq!(e.ii, 1);
assert_eq!(e.num_stages, 1);
}
#[test]
fn test_kexp_total_instructions() {
let h = X86PipelinerHooks::default();
let mut e = X86KernelExpansion::new(2, 2, 5, h);
let instrs = vec![make_mi_def(2, 1), make_mi_def(3, 2)];
let sched = vec![(0, 0, 0), (1, 2, 1)];
e.expand(&sched, &instrs);
assert!(e.total_instructions() > 0);
}
#[test]
fn test_kexp_expansion_factor() {
let h = X86PipelinerHooks::default();
let mut e = X86KernelExpansion::new(2, 2, 5, h);
let instrs = vec![make_mi_def(2, 1), make_mi_def(3, 2)];
let sched = vec![(0, 0, 0), (1, 2, 1)];
e.expand(&sched, &instrs);
let factor = e.code_expansion_factor();
assert!(factor >= 1.0);
}
#[test]
fn test_kexp_predication() {
let mut e = X86KernelExpansion::default();
assert!(!e.use_predication);
e.enable_predication();
assert!(e.use_predication);
}
#[test]
fn test_kexp_mve() {
let h = X86PipelinerHooks::default();
let mut e = X86KernelExpansion::new(2, 2, 5, h);
let mve = ModuloVariableExpansion::new(2, 2, false);
e.set_mve(mve);
assert!(e.mve.is_some());
}
#[test]
fn test_kexp_print_stats() {
let e = X86KernelExpansion::default();
let s = e.print_stats();
assert!(s.contains("X86KernelExpansion"));
}
#[test]
fn test_swp_default() {
let s = X86SoftwarePipelining::default();
assert!(s.enabled);
}
#[test]
fn test_swp_summary_no_pipeline() {
let s = X86SoftwarePipelining::new(X86PipelinerHooks::default());
let summary = s.print_summary();
assert!(summary.contains("Pipelines created: 0"));
}
#[test]
fn test_mp_default() {
let p = X86MachinePipeliner::default();
assert_eq!(p.functions_processed, 0);
}
#[test]
fn test_mp_should_pipeline() {
let p = X86MachinePipeliner::default();
assert!(p.should_pipeline_loop(10, 20, false));
assert!(!p.should_pipeline_loop(1, 5, false));
}
#[test]
fn test_mp_set_max_expansion() {
let mut p = X86MachinePipeliner::default();
p.set_max_expansion(5.0);
assert_eq!(p.max_expansion_factor, 5.0);
}
#[test]
fn test_mp_get_hooks() {
let p = X86MachinePipeliner::new(X86SchedModel::Haswell);
let h = p.get_hooks();
assert_eq!(h.sched_model, X86SchedModel::Haswell);
}
#[test]
fn test_mve_default() {
let m = X86ModuloVariableExpander::default();
assert_eq!(m.new_vregs, 0);
}
#[test]
fn test_mve_rewrite() {
let mut m = X86ModuloVariableExpander::new(2, 3, X86PipelinerHooks::default());
m.expand_value(10, 0, &[1]);
let mut instrs = vec![KernelInstruction {
instr: make_mi_def(2, 10),
stage: 1,
cycle: 0,
is_prologue: false,
is_kernel: true,
is_epilogue: false,
predicate: None,
}];
m.rewrite_instructions(&mut instrs);
assert!(instrs[0].instr.def != Some(10)); }
#[test]
fn test_mve_generate_copies() {
let mut m = X86ModuloVariableExpander::new(2, 3, X86PipelinerHooks::default());
m.expand_value(10, 0, &[1, 2]);
let copies = m.generate_copies();
assert!(!copies.is_empty());
}
#[test]
fn test_mve_print_stats() {
let m = X86ModuloVariableExpander::default();
let s = m.print_stats();
assert!(s.contains("X86MVE"));
}
#[test]
fn test_ims_default() {
let ims = X86IterativeModuloScheduler::default();
assert!(!ims.succeeded);
assert_eq!(ims.attempts, 0);
}
#[test]
fn test_cost_model_default() {
let cm = X86PipeliningCostModel::default();
assert_eq!(cm.trip_count, 10);
}
#[test]
fn test_cost_model_not_profitable_small_trip() {
let mut cm = X86PipeliningCostModel::new(3, 20, 3.0);
cm.set_sizes(4, 20, 10, 10);
cm.evaluate();
assert!(!cm.is_profitable);
}
#[test]
fn test_cost_model_print() {
let cm = X86PipeliningCostModel::default();
let s = cm.print();
assert!(s.contains("CostModel"));
}
#[test]
fn test_verifier_default() {
let v = X86PipelineVerifier::default();
assert!(v.is_valid());
}
#[test]
fn test_verifier_print() {
let mut v = X86PipelineVerifier::new();
v.errors.push("test error".into());
let s = v.print();
assert!(s.contains("ERROR"));
}
#[test]
fn test_pp_clear() {
let mut pp = X86PredicatedPipeliner::default();
let instrs = vec![KernelInstruction {
instr: make_mi(2),
stage: 0,
cycle: 0,
is_prologue: false,
is_kernel: true,
is_epilogue: false,
predicate: None,
}];
pp.predicate_instructions(&instrs, 5);
pp.clear();
assert_eq!(pp.count_predicated(), 0);
}
#[test]
fn test_unroller_default() {
let u = X86LoopUnroller::default();
assert_eq!(u.factor, 2);
}
#[test]
fn test_unroller_factor_3() {
let mut u = X86LoopUnroller::new(3);
let r = u.unroll(&[make_mi(2)]);
assert_eq!(r.len(), 3);
}
#[test]
fn test_bp_default() {
let bp = X86BranchPredictorInfo::default();
assert!(bp.likely_taken);
}
#[test]
fn test_empty_loop_blocks_pipeline() {
let mut lp = X86LoopPipeliner::new(X86SchedModel::Generic);
let mut mf = MachineFunction::new("empty");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![],
successors: vec![],
});
lp.loop_analysis.analyze(&mf);
assert_eq!(lp.loop_analysis.loops.len(), 0);
}
#[test]
fn test_dep_graph_max_nodes() {
let mut g = X86DependenceGraph::new(X86PipelinerHooks::default());
let mut mf = MachineFunction::new("big");
let mut block = MachineBasicBlock {
name: "b0".into(),
instructions: Vec::new(),
successors: vec![],
};
for i in 0..100 {
block.instructions.push(make_mi_def(2, i + 1));
}
mf.blocks.push(block);
g.build(&[0], &mf);
assert_eq!(g.node_count(), 100);
}
#[test]
fn test_res_table_large_ii() {
let res = vec![2; 16];
let rt = ModuloReservationTable::new(64, 16, &res);
assert!(rt.can_place(0b0001, 63));
assert!(rt.can_place(0b0001, 0));
}
#[test]
fn test_hazard_multiple_consumers() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut haz = X86SchedulerHazard::new(hooks);
haz.record_issue(0, 0, 2, Some(1), &[], 0b0000_0001);
let c1 = haz.check_issue(3, Some(10), &[1], 0b0000_0010, 0);
assert!(!c1.can_issue);
let c2 = haz.check_issue(3, Some(11), &[1], 0b0000_0100, 0);
assert!(!c2.can_issue);
}
#[test]
fn test_resmodel_all_skylake_ports() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let rm = X86ResourceModel::new(hooks);
let port_names: Vec<&str> = rm.ports.iter().map(|p| p.name.as_str()).collect();
assert!(port_names.contains(&"Port0"));
assert!(port_names.contains(&"Port7"));
}
#[test]
fn test_pipeline_loop_with_unrolling() {
let mut mf = make_test_loop_mf();
let mut ku = X86KernelUnroller::new(2);
let loop_instrs: Vec<MachineInstr> = mf.blocks[1].instructions.iter().cloned().collect();
let unrolled = ku.unroll(&loop_instrs);
assert!(unrolled.len() >= loop_instrs.len());
assert!(ku.unrolled);
}
#[test]
fn test_loop_pipeliner_all_models() {
let models = [
X86SchedModel::SkylakeClient,
X86SchedModel::Haswell,
X86SchedModel::Zen4,
X86SchedModel::Generic,
];
for &m in &models {
let lp = X86LoopPipeliner::new(m);
let stats = lp.print_stats();
assert!(!stats.is_empty());
}
}
#[test]
fn test_microarch_config_all_models() {
let models = [
X86SchedModel::SandyBridge,
X86SchedModel::IvyBridge,
X86SchedModel::Haswell,
X86SchedModel::Broadwell,
X86SchedModel::SkylakeClient,
X86SchedModel::SkylakeServer,
X86SchedModel::CascadeLake,
X86SchedModel::IceLakeClient,
X86SchedModel::IceLakeServer,
X86SchedModel::TigerLake,
X86SchedModel::SapphireRapids,
X86SchedModel::EmeraldRapids,
X86SchedModel::GraniteRapids,
X86SchedModel::AlderLake,
X86SchedModel::RocketLake,
X86SchedModel::MeteorLake,
X86SchedModel::ArrowLake,
X86SchedModel::LunarLake,
X86SchedModel::Zen1,
X86SchedModel::Zen2,
X86SchedModel::Zen3,
X86SchedModel::Zen4,
X86SchedModel::Zen5,
];
for &m in &models {
let hooks = X86PipelinerHooks::new(m);
assert!(hooks.get_latency(2) > 0);
assert!(hooks.issue_width > 0);
}
}
#[test]
fn test_full_pipeline_workflow() {
let mut mf = MachineFunction::new("full_test");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![make_mi(2)],
successors: vec!["loop_hdr".into()],
});
let mut hdr = MachineBasicBlock {
name: "loop_hdr".into(),
instructions: Vec::new(),
successors: vec!["loop_body".into(), "exit".into()],
};
hdr.instructions.push(make_mi_def(2, 1)); hdr.instructions.push(make_mi(1));
hdr.instructions.push(make_mi_def(3, 2)); hdr.instructions.push(make_mi(STORE_OPCODE));
hdr.instructions.push(make_mi(50));
hdr.instructions.push(make_mi(16)); mf.blocks.push(hdr);
let mut body = MachineBasicBlock {
name: "loop_body".into(),
instructions: Vec::new(),
successors: vec!["loop_hdr".into()],
};
body.instructions.push(make_mi_def(48, 3)); body.instructions.push(make_mi_def(2, 4)); mf.blocks.push(body);
mf.blocks.push(MachineBasicBlock {
name: "exit".into(),
instructions: vec![make_mi(14)],
successors: vec![],
});
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut pipeliner = X86MachinePipeliner::new(X86SchedModel::SkylakeClient);
let result = pipeliner.run_on_function(&mut mf);
assert!(result || !result);
assert_eq!(pipeliner.functions_processed, 1);
}
#[test]
fn test_postra_new() {
let p = X86PostRAPipeliner::new(X86SchedModel::SkylakeClient);
assert!(p.enabled);
assert_eq!(p.blocks_processed, 0);
}
#[test]
fn test_postra_default() {
let p = X86PostRAPipeliner::default();
assert_eq!(p.blocks_processed, 0);
}
#[test]
fn test_postra_run_empty() {
let mut mf = MachineFunction::new("empty");
mf.blocks.push(MachineBasicBlock {
name: "b0".into(),
instructions: vec![],
successors: vec![],
});
let mut p = X86PostRAPipeliner::default();
assert!(!p.run_on_function(&mut mf));
}
#[test]
fn test_postra_run_simple() {
let mut mf = MachineFunction::new("simple");
mf.blocks.push(MachineBasicBlock {
name: "b0".into(),
instructions: vec![make_mi(2), make_mi_def(3, 1), make_mi(1), make_mi_def(2, 2)],
successors: vec![],
});
let mut p = X86PostRAPipeliner::new(X86SchedModel::SkylakeClient);
let result = p.run_on_function(&mut mf);
assert!(result || !result);
}
#[test]
fn test_postra_disabled() {
let mut mf = MachineFunction::new("disabled");
mf.blocks.push(MachineBasicBlock {
name: "b0".into(),
instructions: vec![make_mi(2), make_mi(3)],
successors: vec![],
});
let mut p = X86PostRAPipeliner::default();
p.enabled = false;
assert!(!p.run_on_function(&mut mf));
}
#[test]
fn test_postra_reset() {
let mut p = X86PostRAPipeliner::default();
p.blocks_processed = 10;
p.instructions_reordered = 100;
p.cycles_saved = 5;
p.reset_stats();
assert_eq!(p.blocks_processed, 0);
assert_eq!(p.instructions_reordered, 0);
assert_eq!(p.cycles_saved, 0);
}
#[test]
fn test_postra_print_stats() {
let p = X86PostRAPipeliner::default();
let s = p.print_stats();
assert!(s.contains("X86PostRAPipeliner"));
}
#[test]
fn test_cpr_new() {
let cpr = X86CriticalPathReducer::new();
assert!(!cpr.optimized);
assert_eq!(cpr.original_length, 0);
}
#[test]
fn test_cpr_default() {
let cpr = X86CriticalPathReducer::default();
assert_eq!(cpr.original_length, 0);
}
#[test]
fn test_cpr_analyze() {
let hooks = X86PipelinerHooks::default();
let mut g = X86DependenceGraph::new(hooks);
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
g.compute_asap();
g.compute_alap(100);
g.compute_heights();
g.compute_critical_path();
let mut cpr = X86CriticalPathReducer::new();
cpr.analyze(&g);
assert!(cpr.original_length >= 0);
}
#[test]
fn test_cpr_reduce() {
let hooks = X86PipelinerHooks::default();
let mut g = X86DependenceGraph::new(hooks);
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
g.compute_asap();
g.compute_alap(100);
g.compute_heights();
g.compute_critical_path();
let mut cpr = X86CriticalPathReducer::new();
cpr.analyze(&g);
let reduced = cpr.reduce();
assert!(reduced || !reduced);
}
#[test]
fn test_cpr_reduction_percent() {
let mut cpr = X86CriticalPathReducer::new();
cpr.original_length = 10;
cpr.reduced_length = 8;
assert!(cpr.reduction_percent() > 0.0);
}
#[test]
fn test_cpr_print_summary() {
let cpr = X86CriticalPathReducer::new();
let s = cpr.print_summary();
assert!(s.contains("X86CriticalPathReducer"));
}
#[test]
fn test_config_for_model() {
let cfg = X86PipelinerConfig::for_model(X86SchedModel::SkylakeClient);
assert!(cfg.enable_swp);
assert_eq!(cfg.sched_model, X86SchedModel::SkylakeClient);
}
#[test]
fn test_config_aggressive() {
let cfg = X86PipelinerConfig::aggressive(X86SchedModel::Zen5);
assert!(cfg.enable_swp);
assert!(cfg.enable_predication);
}
#[test]
fn test_config_conservative() {
let cfg = X86PipelinerConfig::conservative(X86SchedModel::Generic);
assert!(!cfg.enable_mve);
assert!(!cfg.enable_predication);
assert_eq!(cfg.min_trip_count, 10);
}
#[test]
fn test_config_default() {
let cfg = X86PipelinerConfig::default();
assert!(cfg.enable_swp);
}
#[test]
fn test_config_print() {
let cfg = X86PipelinerConfig::default();
let s = cfg.print();
assert!(s.contains("X86PipelinerConfig"));
}
#[test]
fn test_stats_new() {
let stats = X86PipelinerStatistics::new();
assert_eq!(stats.functions_processed, 0);
assert_eq!(stats.loops_pipelined, 0);
}
#[test]
fn test_stats_default() {
let stats = X86PipelinerStatistics::default();
assert_eq!(stats.loops_analyzed, 0);
}
#[test]
fn test_stats_record_success() {
let mut stats = X86PipelinerStatistics::new();
stats.record_success(2, 10, 5, 5, 2.0);
assert_eq!(stats.loops_pipelined, 1);
assert_eq!(stats.min_ii_achieved, 2);
assert_eq!(stats.max_ii_achieved, 2);
}
#[test]
fn test_stats_record_multiple_success() {
let mut stats = X86PipelinerStatistics::new();
stats.record_success(3, 10, 4, 3, 1.5);
stats.record_success(5, 12, 5, 4, 1.8);
assert_eq!(stats.loops_pipelined, 2);
assert_eq!(stats.min_ii_achieved, 3);
assert_eq!(stats.max_ii_achieved, 5);
}
#[test]
fn test_stats_record_rejection() {
let mut stats = X86PipelinerStatistics::new();
stats.record_rejection("unprofitable");
stats.record_rejection("schedule_fail");
assert_eq!(stats.loops_rejected_unprofitable, 1);
assert_eq!(stats.loops_rejected_schedule_fail, 1);
}
#[test]
fn test_stats_merge() {
let mut s1 = X86PipelinerStatistics::new();
s1.record_success(2, 10, 3, 3, 1.5);
let mut s2 = X86PipelinerStatistics::new();
s2.record_success(4, 8, 4, 4, 2.0);
s1.merge(&s2);
assert_eq!(s1.loops_pipelined, 2);
}
#[test]
fn test_stats_print() {
let stats = X86PipelinerStatistics::new();
let s = stats.print();
assert!(s.contains("X86PipelinerStatistics"));
assert!(s.contains("Loops analyzed"));
}
#[test]
fn test_dep_graph_duplicate_edges() {
let mut g = X86DependenceGraph::new(X86PipelinerHooks::default());
let mut mf = MachineFunction::new("dup");
mf.blocks.push(MachineBasicBlock {
name: "b0".into(),
instructions: vec![make_mi_def(2, 1), make_mi_def(3, 1)],
successors: vec![],
});
g.build(&[0], &mf);
assert!(g.node_count() > 0);
}
#[test]
fn test_loop_analysis_nested() {
let mut mf = MachineFunction::new("nested");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![],
successors: vec!["outer".into()],
});
mf.blocks.push(MachineBasicBlock {
name: "outer".into(),
instructions: vec![make_mi(2)],
successors: vec!["inner".into(), "exit".into()],
});
mf.blocks.push(MachineBasicBlock {
name: "inner".into(),
instructions: vec![make_mi(3)],
successors: vec!["inner".into(), "outer".into()],
});
mf.blocks.push(MachineBasicBlock {
name: "exit".into(),
instructions: vec![],
successors: vec![],
});
let mut la = X86LoopAnalysis::new();
la.analyze(&mf);
assert!(la.loops.len() >= 1);
}
#[test]
fn test_resource_model_large_mask() {
let hooks = X86PipelinerHooks::new(X86SchedModel::GraniteRapids);
let rm = X86ResourceModel::new(hooks);
assert!(!rm.can_accommodate(u64::MAX, 0) || rm.can_accommodate(u64::MAX, 0));
}
#[test]
fn test_hazard_flags_dependency() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut haz = X86SchedulerHazard::new(hooks);
haz.record_issue(0, 0, 2, Some(1), &[], 0b0000_0001);
let check = haz.check_issue(16, None, &[], 0b0010_0000, 0);
assert!(!check.can_issue);
}
#[test]
fn test_hazard_war_waw() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut haz = X86SchedulerHazard::new(hooks);
haz.record_issue(0, 0, 2, None, &[1], 0b0000_0001);
let check = haz.check_issue(2, Some(1), &[], 0b0000_0010, 1);
assert!(check.can_issue || !check.can_issue);
}
#[test]
fn test_reserve_table_wraparound() {
let res = vec![1; 4];
let mut rt = ModuloReservationTable::new(2, 4, &res);
rt.reserve(0b0001, 4); assert!(!rt.can_place(0b0001, 0));
assert!(!rt.can_place(0b0001, 2));
assert!(rt.can_place(0b0010, 0));
}
#[test]
fn test_pipeliner_config_integration() {
let cfg = X86PipelinerConfig::for_model(X86SchedModel::SkylakeClient);
let hooks = X86PipelinerHooks::new(cfg.sched_model);
let mut swp = X86SoftwarePipelining::new(hooks);
swp.enabled = cfg.enable_swp;
assert!(swp.enabled);
}
#[test]
fn test_full_pipeline_with_unrolling() {
let mut mf = make_test_loop_mf();
let mut unroller = X86LoopUnroller::new(2);
let loop_instrs: Vec<MachineInstr> = mf.blocks[1].instructions.iter().cloned().collect();
let unrolled = unroller.unroll(&loop_instrs);
assert!(unrolled.len() >= loop_instrs.len());
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut swp = X86SoftwarePipelining::new(hooks);
let count = swp.run_on_function(&mut mf);
assert!(count <= 20); }
#[test]
fn test_postra_reordering_preserves_semantics() {
let mut mf = MachineFunction::new("reorder");
mf.blocks.push(MachineBasicBlock {
name: "b0".into(),
instructions: vec![
make_mi_def(2, 1),
make_mi_def(3, 2),
make_mi(1),
make_mi(STORE_OPCODE),
make_mi(50),
make_mi(16),
],
successors: vec![],
});
let mut postra = X86PostRAPipeliner::new(X86SchedModel::SkylakeClient);
let result = postra.run_on_function(&mut mf);
assert!(result || !result);
assert!(!mf.blocks[0].instructions.is_empty());
}
#[test]
fn test_hazard_detector_integration() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut haz = X86SchedulerHazard::new(hooks);
haz.record_issue(0, 0, 2, Some(1), &[], 0b0000_0001);
haz.advance_cycle(1);
let c1 = haz.check_issue(3, Some(2), &[1], 0b0000_0001, 1);
assert!(c1.can_issue);
haz.record_issue(1, 1, 3, Some(2), &[1], 0b0000_0010);
haz.advance_cycle(2);
let c2 = haz.check_issue(16, None, &[], 0b0010_0000, 2);
assert!(c2.can_issue || !c2.can_issue);
}
#[test]
fn test_statistics_merge_comprehensive() {
let mut s1 = X86PipelinerStatistics::new();
s1.functions_processed = 5;
s1.loops_analyzed = 20;
s1.loops_pipelined = 15;
s1.record_success(2, 10, 3, 3, 1.5);
let mut s2 = X86PipelinerStatistics::new();
s2.functions_processed = 3;
s2.loops_analyzed = 10;
s2.loops_pipelined = 8;
s2.record_success(4, 8, 4, 4, 2.0);
s1.merge(&s2);
assert_eq!(s1.functions_processed, 8);
assert_eq!(s1.loops_analyzed, 30);
assert_eq!(s1.loops_pipelined, 25);
}
#[test]
fn test_loop_pipeliner_with_config() {
let cfg = X86PipelinerConfig::aggressive(X86SchedModel::SkylakeClient);
let mut lp = X86LoopPipeliner::new(cfg.sched_model);
lp.hazard = X86SchedulerHazard::new(lp.hooks.clone());
lp.resource_model = X86ResourceModel::new(lp.hooks.clone());
if cfg.enable_unrolling {
lp.unroller = X86LoopUnroller::new(cfg.unroll_factor);
}
let mut mf = make_test_loop_mf();
lp.loop_analysis.analyze(&mf);
if let Some(loop_info) = lp.loop_analysis.loops.first().cloned() {
let result = lp.pipeline_loop(&loop_info, &mut mf);
assert!(result || !result);
}
}
#[test]
fn test_zero_trip_count() {
let hooks = X86PipelinerHooks::default();
assert!(!hooks.should_pipeline(0, 10, false));
}
#[test]
fn test_max_ii_boundary() {
let s = X86ModuloScheduler::default();
assert!(s.max_ii >= 1);
}
#[test]
fn test_invalid_resource_mask() {
let res = vec![1; 4];
let rt = ModuloReservationTable::new(2, 4, &res);
assert!(rt.can_place(0, 0));
assert!(rt.can_place(0, 100));
}
#[test]
fn test_large_loop_hundred_instructions() {
let mut mf = MachineFunction::new("big_loop");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![],
successors: vec!["loop".into()],
});
let mut lb = MachineBasicBlock {
name: "loop".into(),
instructions: Vec::new(),
successors: vec!["loop".into(), "exit".into()],
};
for i in 0..100 {
lb.instructions.push(make_mi_def(2, i + 1));
}
lb.instructions.push(make_mi(50));
lb.instructions.push(make_mi(16));
mf.blocks.push(lb);
mf.blocks.push(MachineBasicBlock {
name: "exit".into(),
instructions: vec![],
successors: vec![],
});
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut g = X86DependenceGraph::new(hooks);
g.build(&[1], &mf);
assert_eq!(g.node_count(), 102);
let rec_mii = g.compute_rec_mii();
assert!(rec_mii >= 1);
}
#[test]
fn test_structural_hazard_exhaustion() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let mut haz = X86SchedulerHazard::new(hooks);
for i in 0..4 {
haz.record_issue(i as usize, 0, 2, Some(i + 1), &[], 0b0000_0001);
}
let check = haz.check_issue(2, Some(10), &[], 0b0000_0001, 0);
assert!(!check.can_issue);
let structural = haz
.detected_hazards
.iter()
.filter(|h| matches!(h.kind, HazardKind::StructuralPort))
.count();
assert!(structural > 0);
}
#[test]
fn prop_res_mii_at_least_one() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
let rm = X86ResourceModel::new(hooks);
assert!(rm.compute_res_mii(&[2]) >= 1);
assert!(rm.compute_res_mii(&[]) >= 1);
}
#[test]
fn prop_latency_table_nonzero() {
for model in &[
X86SchedModel::SkylakeClient,
X86SchedModel::Zen4,
X86SchedModel::Haswell,
X86SchedModel::Generic,
] {
let hooks = X86PipelinerHooks::new(*model);
assert!(hooks.get_latency(2) > 0);
assert!(hooks.get_latency(1) > 0);
assert!(hooks.get_latency(STORE_OPCODE) > 0);
}
}
#[test]
fn prop_unroll_factor_preserves_count() {
for f in &[1, 2, 4, 8] {
let mut ku = X86KernelUnroller::new(*f);
let input = vec![make_mi(2), make_mi(3), make_mi(6)];
let output = ku.unroll(&input);
assert_eq!(output.len(), input.len() * *f as usize);
}
}
#[test]
fn prop_schedule_length_matches_nodes() {
let hooks = X86PipelinerHooks::default();
let mut g = X86DependenceGraph::new(hooks.clone());
let mf = make_test_loop_mf();
g.build(&[1, 2], &mf);
g.compute_asap();
g.compute_alap(100);
let mut d = X86ScheduleDAG::new(hooks.clone());
d.build_from_dep_graph(&g);
let mut s = X86ModuloScheduler::new(hooks, d, g);
s.schedule();
let sched = s.get_schedule();
assert_eq!(sched.len(), s.dag.nodes.len());
}
#[test]
fn prop_expansion_factor_positive() {
let h = X86PipelinerHooks::default();
let e = X86KernelExpansion::new(2, 4, 10, h);
assert!(e.code_expansion_factor() >= 1.0);
}
#[test]
fn test_skylake_has_8_ports() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SkylakeClient);
assert_eq!(hooks.num_ports, 8);
assert_eq!(hooks.issue_width, 4);
}
#[test]
fn test_zen4_has_avx512() {
let hooks = X86PipelinerHooks::new(X86SchedModel::Zen4);
assert!(hooks.has_avx512);
assert!(hooks.has_avx2);
assert!(hooks.has_avx);
}
#[test]
fn test_sapphire_rapids_wide_issue() {
let hooks = X86PipelinerHooks::new(X86SchedModel::SapphireRapids);
assert!(hooks.issue_width >= 6);
assert!(hooks.num_ports >= 12);
}
#[test]
fn test_granite_rapids_max_config() {
let hooks = X86PipelinerHooks::new(X86SchedModel::GraniteRapids);
assert!(hooks.issue_width >= 6);
assert!(hooks.rob_size >= 500);
}
#[test]
fn test_all_components_instantiable() {
let _h = X86PipelinerHooks::default();
let _la = X86LoopAnalysis::default();
let _dg = X86DependenceGraph::default();
let _sd = X86ScheduleDAG::default();
let _ms = X86ModuloScheduler::default();
let _ke = X86KernelExpansion::default();
let _sp = X86SoftwarePipelining::default();
let _mp = X86MachinePipeliner::default();
let _mv = X86ModuloVariableExpander::default();
let _im = X86IterativeModuloScheduler::default();
let _cm = X86PipeliningCostModel::default();
let _pv = X86PipelineVerifier::default();
let _pp = X86PredicatedPipeliner::default();
let _lu = X86LoopUnroller::default();
let _bp = X86BranchPredictorInfo::default();
let _hz = X86SchedulerHazard::default();
let _ha = HazardAwareScheduler::default();
let _rm = X86ResourceModel::default();
let _da = X86DependenceAnalysis::default();
let _lp = X86LoopPipeliner::default();
let _rp = X86RegPressureAwareScheduler::default();
let _ku = X86KernelUnroller::default();
let _pr = X86PostRAPipeliner::default();
let _cr = X86CriticalPathReducer::default();
let _cf = X86PipelinerConfig::default();
let _st = X86PipelinerStatistics::default();
}
}