use crate::x86::x86_instr_info::{X86InstrInfo, X86Opcode};
use crate::x86::x86_subtarget::X86Subtarget;
use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum SLPOpcode {
Load,
Store,
Add,
Sub,
Mul,
UDiv,
SDiv,
URem,
SRem,
And,
Or,
Xor,
Shl,
LShr,
AShr,
FAdd,
FSub,
FMul,
FDiv,
FRem,
ICmp,
FCmp,
Select,
Trunc,
ZExt,
SExt,
FPTrunc,
FPExt,
FPToUI,
FPToSI,
UIToFP,
SIToFP,
BitCast,
PHI,
Call,
ExtractElement,
InsertElement,
ShuffleVector,
Unknown,
}
impl SLPOpcode {
pub fn is_commutative(self) -> bool {
matches!(
self,
SLPOpcode::Add
| SLPOpcode::Mul
| SLPOpcode::And
| SLPOpcode::Or
| SLPOpcode::Xor
| SLPOpcode::FAdd
| SLPOpcode::FMul
| SLPOpcode::ICmp
| SLPOpcode::FCmp
)
}
pub fn is_slp_candidate(self) -> bool {
!matches!(self, SLPOpcode::Unknown | SLPOpcode::PHI | SLPOpcode::Call)
}
pub fn from_str(s: &str) -> Self {
match s {
"load" => SLPOpcode::Load,
"store" => SLPOpcode::Store,
"add" | "add nsw" | "add nuw" => SLPOpcode::Add,
"sub" | "sub nsw" | "sub nuw" => SLPOpcode::Sub,
"mul" | "mul nsw" | "mul nuw" => SLPOpcode::Mul,
"udiv" => SLPOpcode::UDiv,
"sdiv" => SLPOpcode::SDiv,
"urem" => SLPOpcode::URem,
"srem" => SLPOpcode::SRem,
"and" => SLPOpcode::And,
"or" => SLPOpcode::Or,
"xor" => SLPOpcode::Xor,
"shl" => SLPOpcode::Shl,
"lshr" => SLPOpcode::LShr,
"ashr" => SLPOpcode::AShr,
"fadd" => SLPOpcode::FAdd,
"fsub" => SLPOpcode::FSub,
"fmul" => SLPOpcode::FMul,
"fdiv" => SLPOpcode::FDiv,
"frem" => SLPOpcode::FRem,
"icmp" => SLPOpcode::ICmp,
"fcmp" => SLPOpcode::FCmp,
"select" => SLPOpcode::Select,
"trunc" => SLPOpcode::Trunc,
"zext" => SLPOpcode::ZExt,
"sext" => SLPOpcode::SExt,
"fptrunc" => SLPOpcode::FPTrunc,
"fpext" => SLPOpcode::FPExt,
"fptoui" => SLPOpcode::FPToUI,
"fptosi" => SLPOpcode::FPToSI,
"uitofp" => SLPOpcode::UIToFP,
"sitofp" => SLPOpcode::SIToFP,
"bitcast" => SLPOpcode::BitCast,
"phi" => SLPOpcode::PHI,
"call" => SLPOpcode::Call,
"extractelement" => SLPOpcode::ExtractElement,
"insertelement" => SLPOpcode::InsertElement,
"shufflevector" => SLPOpcode::ShuffleVector,
_ => SLPOpcode::Unknown,
}
}
}
#[derive(Debug, Clone)]
pub struct SLPLane {
pub value: String,
pub operands: Vec<usize>,
}
#[derive(Debug, Clone)]
pub struct SLPTreeNode {
pub id: usize,
pub opcode: SLPOpcode,
pub lanes: Vec<SLPLane>,
pub children: Vec<usize>,
pub depth: usize,
pub subtree_depth: usize,
pub element_count: usize,
pub is_vectorizable: bool,
pub is_uniform: bool,
pub needs_reorder: bool,
pub reorder_map: Option<Vec<usize>>,
pub vector_width_bits: u32,
pub cost: f64,
pub is_pruned: bool,
pub is_reduction: bool,
pub reduction_kind: Option<SLPReductionKind>,
}
impl SLPTreeNode {
pub fn new_leaf(id: usize, opcode: SLPOpcode, values: Vec<String>) -> Self {
let element_count = values.len();
let lanes = values
.into_iter()
.map(|v| SLPLane {
value: v,
operands: vec![],
})
.collect();
SLPTreeNode {
id,
opcode,
lanes,
children: vec![],
depth: 0,
subtree_depth: 0,
element_count,
is_vectorizable: opcode.is_slp_candidate(),
is_uniform: true,
needs_reorder: false,
reorder_map: None,
vector_width_bits: 0,
cost: 0.0,
is_pruned: false,
is_reduction: false,
reduction_kind: None,
}
}
pub fn new_internal(
id: usize,
opcode: SLPOpcode,
lanes: Vec<SLPLane>,
children: Vec<usize>,
) -> Self {
let element_count = lanes.len();
SLPTreeNode {
id,
opcode,
lanes,
children,
depth: 0,
subtree_depth: 0,
element_count,
is_vectorizable: opcode.is_slp_candidate(),
is_uniform: true,
needs_reorder: false,
reorder_map: None,
vector_width_bits: 0,
cost: 0.0,
is_pruned: false,
is_reduction: false,
reduction_kind: None,
}
}
pub fn is_leaf(&self) -> bool {
self.children.is_empty()
}
pub fn lane_count(&self) -> usize {
self.lanes.len()
}
}
#[derive(Debug, Clone)]
pub struct SLPTree {
pub nodes: BTreeMap<usize, SLPTreeNode>,
pub root_id: usize,
next_id: usize,
pub stats: SLPTreeStats,
}
#[derive(Debug, Clone, Default)]
pub struct SLPTreeStats {
pub total_nodes: usize,
pub vectorizable_nodes: usize,
pub pruned_nodes: usize,
pub reduction_nodes: usize,
pub max_depth: usize,
pub total_lanes: usize,
pub reordered_nodes: usize,
}
impl SLPTree {
pub fn new() -> Self {
SLPTree {
nodes: BTreeMap::new(),
root_id: 0,
next_id: 1,
stats: SLPTreeStats::default(),
}
}
fn alloc_id(&mut self) -> usize {
let id = self.next_id;
self.next_id += 1;
id
}
pub fn add_node(&mut self, node: SLPTreeNode) -> usize {
let id = node.id;
self.stats.total_nodes += 1;
if node.is_vectorizable {
self.stats.vectorizable_nodes += 1;
}
self.stats.total_lanes += node.lane_count();
self.nodes.insert(id, node);
id
}
pub fn get_node(&self, id: usize) -> Option<&SLPTreeNode> {
self.nodes.get(&id)
}
pub fn get_node_mut(&mut self, id: usize) -> Option<&mut SLPTreeNode> {
self.nodes.get_mut(&id)
}
pub fn compute_depths(&mut self) {
let mut queue = VecDeque::new();
queue.push_back((self.root_id, 0));
while let Some((node_id, depth)) = queue.pop_front() {
let child_ids = if let Some(node) = self.nodes.get(&node_id) {
node.children.clone()
} else {
continue;
};
for child_id in &child_ids {
queue.push_back((*child_id, depth + 1));
}
let mut max_child_depth = 0;
for child_id in &child_ids {
if let Some(child) = self.nodes.get(child_id) {
max_child_depth = max_child_depth.max(child.subtree_depth);
}
}
if let Some(node) = self.nodes.get_mut(&node_id) {
node.depth = depth;
node.subtree_depth = if child_ids.is_empty() {
0
} else {
max_child_depth + 1
};
self.stats.max_depth = self.stats.max_depth.max(node.subtree_depth);
}
}
}
pub fn prune_subtree(&mut self, node_id: usize) {
let mut to_prune = VecDeque::new();
to_prune.push_back(node_id);
while let Some(id) = to_prune.pop_front() {
if let Some(node) = self.nodes.get_mut(&id) {
if !node.is_pruned {
node.is_pruned = true;
self.stats.pruned_nodes += 1;
let children = node.children.clone();
for child in children {
to_prune.push_back(child);
}
}
}
}
}
pub fn bfs_iter(&self) -> Vec<usize> {
let mut order = Vec::new();
let mut queue = VecDeque::new();
queue.push_back(self.root_id);
let mut visited = HashSet::new();
while let Some(id) = queue.pop_front() {
if visited.contains(&id) {
continue;
}
visited.insert(id);
if let Some(node) = self.nodes.get(&id) {
if !node.is_pruned {
order.push(id);
for child in &node.children {
queue.push_back(*child);
}
}
}
}
order
}
}
#[derive(Debug, Clone)]
pub struct StoreChain {
pub stores: Vec<StoreDescriptor>,
pub base_ptr: String,
pub stride: i64,
pub element_size: u32,
pub is_consecutive: bool,
pub alignment: u32,
}
#[derive(Debug, Clone)]
pub struct StoreDescriptor {
pub inst_idx: usize,
pub stored_value: String,
pub pointer: String,
pub offset: i64,
}
pub fn detect_store_chains(instructions: &[(usize, &str, Vec<&str>)]) -> Vec<StoreChain> {
let mut chains: Vec<StoreChain> = Vec::new();
let mut by_ptr: HashMap<String, Vec<(usize, String, i64, u32)>> = HashMap::new();
for &(idx, opcode, ref operands) in instructions {
if opcode == "store" && operands.len() >= 2 {
let val = operands[0].to_string();
let ptr = operands[1].to_string();
let offset: i64 = if operands.len() >= 3 {
operands[2].parse().unwrap_or(0)
} else {
0
};
let elem_size = 4u32;
by_ptr
.entry(ptr.clone())
.or_default()
.push((idx, val, offset, elem_size));
}
}
for (ptr, mut entries) in by_ptr {
entries.sort_by_key(|e| e.2); if entries.len() < 2 {
continue;
}
let stride = entries[1].2 - entries[0].2;
let is_consecutive = entries.windows(2).all(|w| w[1].2 - w[0].2 == stride);
let stores: Vec<StoreDescriptor> = entries
.iter()
.map(|&(idx, ref val, offset, _elem_size)| StoreDescriptor {
inst_idx: idx,
stored_value: val.clone(),
pointer: ptr.clone(),
offset,
})
.collect();
let alignment = stores
.iter()
.map(|s| {
let abs_off = s.offset.unsigned_abs() as u32;
if abs_off == 0 {
64
} else {
1 << abs_off.trailing_zeros()
}
})
.max()
.unwrap_or(4);
chains.push(StoreChain {
stores,
base_ptr: ptr,
stride,
element_size: 4, is_consecutive,
alignment,
});
}
chains
}
pub fn bundle_operands(instructions: &[(usize, SLPOpcode, Vec<String>)]) -> Vec<Vec<String>> {
if instructions.is_empty() {
return vec![];
}
let arity = instructions[0].2.len();
if !instructions.iter().all(|i| i.2.len() == arity) {
return vec![];
}
let mut bundles = vec![Vec::new(); arity];
for (_, _, operands) in instructions {
for (j, op) in operands.iter().enumerate() {
bundles[j].push(op.clone());
}
}
bundles
}
pub fn can_bundle(values: &[String], seen: &HashSet<String>) -> bool {
if values.len() < 2 {
return false;
}
let mut unique = HashSet::new();
for v in values {
if !unique.insert(v.clone()) {
}
}
values
.iter()
.all(|v| seen.contains(v) || v.starts_with("const") || v.starts_with("undef"))
}
#[derive(Debug, Clone)]
pub struct ReorderResult {
pub permutation: Vec<usize>,
pub shuffle_cost: usize,
pub is_profitable: bool,
}
pub fn find_optimal_reorder(bundle_a: &[String], bundle_b: &[String]) -> ReorderResult {
if bundle_a.len() != bundle_b.len() || bundle_a.is_empty() {
return ReorderResult {
permutation: vec![],
shuffle_cost: 0,
is_profitable: false,
};
}
let n = bundle_a.len();
let mut b_positions: HashMap<String, Vec<usize>> = HashMap::new();
for (i, val) in bundle_b.iter().enumerate() {
b_positions.entry(val.clone()).or_default().push(i);
}
let mut used = vec![false; n];
let mut permutation = vec![0usize; n];
let mut shuffle_cost = 0;
for i in 0..n {
let a_val = &bundle_a[i];
if let Some(positions) = b_positions.get(a_val) {
let mut matched = false;
for &pos in positions {
if !used[pos] {
permutation[i] = pos;
used[pos] = true;
matched = true;
break;
}
}
if !matched {
for j in 0..n {
if !used[j] {
permutation[i] = j;
used[j] = true;
shuffle_cost += 1;
break;
}
}
}
} else {
for j in 0..n {
if !used[j] {
permutation[i] = j;
used[j] = true;
shuffle_cost += 1;
break;
}
}
}
}
let is_identity = permutation.iter().enumerate().all(|(i, &p)| i == p);
ReorderResult {
permutation,
shuffle_cost,
is_profitable: !is_identity && shuffle_cost < n / 2,
}
}
#[derive(Debug, Clone, Default)]
pub struct SLPBuildContext {
pub defs: HashMap<String, (SLPOpcode, Vec<String>)>,
pub visited: HashSet<String>,
pub max_depth: usize,
pub current_depth: usize,
pub allow_reorder: bool,
}
pub fn build_slp_tree_bottom_up(stores: &StoreChain, ctx: &mut SLPBuildContext) -> SLPTree {
let mut tree = SLPTree::new();
let store_values: Vec<String> = stores
.stores
.iter()
.map(|s| s.stored_value.clone())
.collect();
let root = SLPTreeNode::new_leaf(tree.alloc_id(), SLPOpcode::Store, store_values.clone());
let root_id = tree.add_node(root);
tree.root_id = root_id;
for v in &store_values {
ctx.visited.insert(v.clone());
}
let mut worklist: VecDeque<(usize, Vec<String>)> = VecDeque::new();
worklist.push_back((root_id, store_values));
while let Some((parent_id, values)) = worklist.pop_front() {
if ctx.current_depth >= ctx.max_depth {
break;
}
let mut defs: Vec<Option<&(SLPOpcode, Vec<String>)>> = Vec::new();
for v in &values {
defs.push(ctx.defs.get(v));
}
let first_opcode = defs.iter().find_map(|d| d.map(|d| d.0));
if first_opcode.is_none() {
continue;
}
let opcode = first_opcode.unwrap();
let all_same_opcode = defs.iter().all(|d| d.map_or(false, |d| d.0 == opcode));
let all_defined = defs.iter().all(|d| d.is_some());
if !all_same_opcode || !all_defined {
continue;
}
let arities: Vec<usize> = defs.iter().filter_map(|d| d.map(|d| d.1.len())).collect();
if arities.is_empty() || !arities.iter().all(|&a| a == arities[0]) {
continue;
}
let arity = arities[0];
let mut operand_bundles: Vec<Vec<String>> = vec![Vec::new(); arity];
for d in &defs {
if let Some((_, operands)) = d {
for (j, op) in operands.iter().enumerate() {
operand_bundles[j].push(op.clone());
}
}
}
let mut child_ids = Vec::new();
for bundle in &operand_bundles {
if bundle.len() < 2 || !opcode.is_slp_candidate() {
continue;
}
let mut final_bundle = bundle.clone();
let needs_reorder = if ctx.allow_reorder && bundle.len() >= 2 {
let has_duplicate_operands = (0..bundle.len())
.any(|i| bundle.iter().filter(|v| *v == &bundle[i]).count() > 1);
has_duplicate_operands
} else {
false
};
let child = SLPTreeNode {
id: tree.alloc_id(),
opcode,
lanes: final_bundle
.iter()
.map(|v| SLPLane {
value: v.clone(),
operands: vec![],
})
.collect(),
children: vec![],
depth: 0,
subtree_depth: 0,
element_count: final_bundle.len(),
is_vectorizable: opcode.is_slp_candidate(),
is_uniform: true,
needs_reorder,
reorder_map: None,
vector_width_bits: 0,
cost: 0.0,
is_pruned: false,
is_reduction: false,
reduction_kind: None,
};
let child_id = tree.add_node(child);
child_ids.push(child_id);
for v in &final_bundle {
ctx.visited.insert(v.clone());
}
ctx.current_depth += 1;
worklist.push_back((child_id, final_bundle));
}
if let Some(parent) = tree.nodes.get_mut(&parent_id) {
parent.children = child_ids;
}
}
tree.compute_depths();
tree
}
pub fn analyze_slp_tree_top_down(tree: &mut SLPTree, isa_width_bits: u32) {
let mut queue = VecDeque::new();
queue.push_back(tree.root_id);
let mut visited = HashSet::new();
while let Some(node_id) = queue.pop_front() {
if visited.contains(&node_id) {
continue;
}
visited.insert(node_id);
if let Some(node) = tree.nodes.get_mut(&node_id) {
if node.is_pruned {
continue;
}
let elem_bits = 32u32; let needed_bits = node.element_count as u32 * elem_bits;
let mut vec_bits = if needed_bits >= 512 {
512
} else if needed_bits >= 256 {
256
} else if needed_bits >= 128 {
128
} else if needed_bits >= 64 {
64
} else {
elem_bits
};
vec_bits = vec_bits.min(isa_width_bits);
if vec_bits > 64 {
vec_bits = vec_bits.next_power_of_two().min(isa_width_bits);
}
let min_elements = vec_bits / elem_bits;
if node.element_count < min_elements as usize || node.element_count < 2 {
node.is_vectorizable = false;
}
node.vector_width_bits = vec_bits;
let children = node.children.clone();
for child in children {
queue.push_back(child);
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SLPMultiNodePattern {
ComplexNumbers,
RGBAColors,
Vectors3D,
Matrix2x2,
Quaternion,
StructOfArrays,
InterleavedAccess,
StridedAccess,
}
impl SLPMultiNodePattern {
pub fn min_elements(self) -> usize {
match self {
SLPMultiNodePattern::ComplexNumbers => 2,
SLPMultiNodePattern::RGBAColors => 4,
SLPMultiNodePattern::Vectors3D => 3,
SLPMultiNodePattern::Matrix2x2 => 4,
SLPMultiNodePattern::Quaternion => 4,
SLPMultiNodePattern::StructOfArrays => 2,
SLPMultiNodePattern::InterleavedAccess => 2,
SLPMultiNodePattern::StridedAccess => 2,
}
}
pub fn required_isa(self) -> &'static str {
match self {
SLPMultiNodePattern::ComplexNumbers => "SSE2",
SLPMultiNodePattern::RGBAColors => "SSE2",
SLPMultiNodePattern::Vectors3D => "SSE2",
SLPMultiNodePattern::Matrix2x2 => "SSE2",
SLPMultiNodePattern::Quaternion => "SSE2",
SLPMultiNodePattern::StructOfArrays => "SSE",
SLPMultiNodePattern::InterleavedAccess => "SSE2",
SLPMultiNodePattern::StridedAccess => "SSE2",
}
}
}
pub fn detect_slp_patterns(
defs: &HashMap<String, (SLPOpcode, Vec<String>)>,
) -> Vec<(SLPMultiNodePattern, Vec<String>)> {
let mut patterns: Vec<(SLPMultiNodePattern, Vec<String>)> = Vec::new();
let mut fadd_pairs: HashMap<String, Vec<String>> = HashMap::new();
let mut fsub_pairs: HashMap<String, Vec<String>> = HashMap::new();
for (name, (opcode, operands)) in defs {
match opcode {
SLPOpcode::FAdd if operands.len() == 2 => {
fadd_pairs
.entry(operands[0].clone())
.or_default()
.push(name.clone());
fadd_pairs
.entry(operands[1].clone())
.or_default()
.push(name.clone());
}
SLPOpcode::FSub if operands.len() == 2 => {
fsub_pairs
.entry(operands[0].clone())
.or_default()
.push(name.clone());
fsub_pairs
.entry(operands[1].clone())
.or_default()
.push(name.clone());
}
_ => {}
}
}
for (_, ref names) in &fadd_pairs {
if names.len() >= 2 {
let mut group = names.to_vec();
group.sort();
let has_fsub = names.iter().any(|n| {
if let Some((opcode, _)) = defs.get(n) {
*opcode == SLPOpcode::FSub
} else {
false
}
});
if has_fsub && group.len() >= 2 {
patterns.push((SLPMultiNodePattern::ComplexNumbers, group));
}
}
}
let mut streams: HashMap<String, Vec<String>> = HashMap::new();
for (name, (_opcode, operands)) in defs {
for op in operands {
streams.entry(op.clone()).or_default().push(name.clone());
}
}
for (_, ref names) in &streams {
let mut sorted = names.to_vec();
sorted.sort();
sorted.dedup();
if sorted.len() >= 4 {
patterns.push((SLPMultiNodePattern::RGBAColors, sorted));
break; }
}
if let Some(first_name) = defs.keys().next().cloned() {
let mut group = vec![first_name];
if group.len() >= 3 {
patterns.push((SLPMultiNodePattern::Vectors3D, group));
}
}
patterns
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SLPReductionKind {
HAdd,
HSub,
HMul,
DotProduct,
SAD,
HMin,
HMax,
WidenSum,
}
impl SLPReductionKind {
pub fn sse_instruction(self) -> &'static str {
match self {
SLPReductionKind::HAdd => "haddps",
SLPReductionKind::HSub => "hsubps",
SLPReductionKind::DotProduct => "dpps",
SLPReductionKind::SAD => "psadbw",
SLPReductionKind::HMul => "mulps+vperm",
SLPReductionKind::HMin => "minps",
SLPReductionKind::HMax => "maxps",
SLPReductionKind::WidenSum => "pmaddwd",
}
}
pub fn has_native_support(self) -> bool {
matches!(
self,
SLPReductionKind::HAdd
| SLPReductionKind::HSub
| SLPReductionKind::DotProduct
| SLPReductionKind::SAD
| SLPReductionKind::WidenSum
)
}
pub fn min_elements(self) -> usize {
match self {
SLPReductionKind::HAdd | SLPReductionKind::HSub => 2,
SLPReductionKind::DotProduct => 4,
SLPReductionKind::SAD => 8,
SLPReductionKind::HMul => 2,
SLPReductionKind::HMin | SLPReductionKind::HMax => 2,
SLPReductionKind::WidenSum => 4,
}
}
}
pub fn recognize_horizontal_reduction(
tree: &mut SLPTree,
node_id: usize,
) -> Option<SLPReductionKind> {
let node = tree.nodes.get(&node_id)?;
let reduction_kind = match node.opcode {
SLPOpcode::Add | SLPOpcode::FAdd => {
if node.lane_count() >= 4 {
Some(SLPReductionKind::HAdd)
} else {
None
}
}
SLPOpcode::Sub | SLPOpcode::FSub => {
if node.lane_count() >= 2 {
Some(SLPReductionKind::HSub)
} else {
None
}
}
SLPOpcode::Mul | SLPOpcode::FMul => {
if node.lane_count() >= 4 && !node.children.is_empty() {
let has_load_child = node.children.iter().any(|&cid| {
tree.nodes
.get(&cid)
.map_or(false, |c| c.opcode == SLPOpcode::Load)
});
if has_load_child {
Some(SLPReductionKind::DotProduct)
} else {
Some(SLPReductionKind::HMul)
}
} else if node.lane_count() >= 2 {
Some(SLPReductionKind::HMul)
} else {
None
}
}
_ => None,
};
if let Some(kind) = reduction_kind {
if let Some(node) = tree.nodes.get_mut(&node_id) {
node.is_reduction = true;
node.reduction_kind = Some(kind);
}
tree.stats.reduction_nodes += 1;
}
reduction_kind
}
#[derive(Debug, Clone)]
pub struct SLPCostModel {
pub isa_width_bits: u32,
pub vector_load_cost: f64,
pub vector_store_cost: f64,
pub vector_arith_cost: f64,
pub vector_mul_cost: f64,
pub vector_div_cost: f64,
pub shuffle_cost: f64,
pub extract_cost: f64,
pub insert_cost: f64,
pub broadcast_cost: f64,
pub gather_cost: f64,
pub profit_threshold: f64,
}
impl SLPCostModel {
pub fn new(isa_width_bits: u32) -> Self {
let (vload, vstore, varith, vmul, vdiv) = match isa_width_bits {
512 => (5.0, 4.0, 1.0, 2.0, 10.0),
256 => (4.0, 3.0, 1.0, 2.0, 12.0),
128 => (3.5, 2.5, 1.0, 2.0, 14.0),
_ => (3.0, 2.0, 1.0, 2.0, 20.0),
};
SLPCostModel {
isa_width_bits,
vector_load_cost: vload,
vector_store_cost: vstore,
vector_arith_cost: varith,
vector_mul_cost: vmul,
vector_div_cost: vdiv,
shuffle_cost: 1.0,
extract_cost: 1.0,
insert_cost: 1.0,
broadcast_cost: 0.5,
gather_cost: 4.0,
profit_threshold: 0.75, }
}
pub fn compute_node_cost(&self, node: &SLPTreeNode) -> f64 {
if !node.is_vectorizable || node.is_pruned {
return f64::MAX;
}
let element_count = node.element_count as f64;
let base_cost = match node.opcode {
SLPOpcode::Load => self.vector_load_cost,
SLPOpcode::Store => self.vector_store_cost,
SLPOpcode::Add | SLPOpcode::Sub | SLPOpcode::FAdd | SLPOpcode::FSub => {
self.vector_arith_cost
}
SLPOpcode::Mul | SLPOpcode::FMul => self.vector_mul_cost,
SLPOpcode::UDiv | SLPOpcode::SDiv | SLPOpcode::FDiv => self.vector_div_cost,
SLPOpcode::And | SLPOpcode::Or | SLPOpcode::Xor => self.vector_arith_cost,
SLPOpcode::Shl | SLPOpcode::LShr | SLPOpcode::AShr => self.vector_arith_cost * 1.5,
SLPOpcode::ICmp | SLPOpcode::FCmp => self.vector_arith_cost,
SLPOpcode::Select => self.vector_arith_cost * 2.0,
SLPOpcode::ExtractElement => self.extract_cost,
SLPOpcode::InsertElement => self.insert_cost,
SLPOpcode::ShuffleVector => self.shuffle_cost,
_ => self.vector_arith_cost,
};
let reorder_penalty = if node.needs_reorder {
self.shuffle_cost * 2.0
} else {
0.0
};
let reduction_bonus = if node.is_reduction {
-(self.vector_arith_cost * (element_count - 1.0))
} else {
0.0
};
base_cost + reorder_penalty + reduction_bonus
}
pub fn compute_tree_cost(&self, tree: &SLPTree) -> f64 {
let mut total = 0.0;
for node in tree.nodes.values() {
if !node.is_pruned {
total += self.compute_node_cost(node);
}
}
total
}
pub fn compute_scalar_cost(&self, tree: &SLPTree) -> f64 {
let mut total = 0.0;
for node in tree.nodes.values() {
if node.is_pruned {
continue;
}
let scalar_per_op = match node.opcode {
SLPOpcode::Load => 3.0,
SLPOpcode::Store => 2.0,
SLPOpcode::FDiv | SLPOpcode::SDiv | SLPOpcode::UDiv => 14.0,
SLPOpcode::FMul | SLPOpcode::Mul => 4.0,
_ => 1.0,
};
total += scalar_per_op * node.element_count as f64;
}
total
}
pub fn is_profitable(&self, tree: &SLPTree) -> bool {
let vec_cost = self.compute_tree_cost(tree);
let scalar_cost = self.compute_scalar_cost(tree);
if scalar_cost <= 0.0 {
return false;
}
vec_cost / scalar_cost < self.profit_threshold
}
}
#[derive(Debug, Clone)]
pub struct ScheduledSLPOp {
pub opcode: SLPOpcode,
pub result: String,
pub operands: Vec<String>,
pub element_count: usize,
pub element_type: String,
pub is_reduction: bool,
pub reduction_kind: Option<SLPReductionKind>,
pub needs_shuffle: bool,
pub shuffle_mask: Option<Vec<usize>>,
}
pub fn schedule_slp_tree(tree: &SLPTree) -> Vec<ScheduledSLPOp> {
let mut schedule = Vec::new();
let order = tree.bfs_iter();
for node_id in order.iter().rev() {
if let Some(node) = tree.nodes.get(node_id) {
if node.is_pruned || !node.is_vectorizable {
continue;
}
let result = format!("slp.{}", node_id);
let operands: Vec<String> = node
.children
.iter()
.map(|cid| format!("slp.{}", cid))
.collect();
schedule.push(ScheduledSLPOp {
opcode: node.opcode,
result,
operands,
element_count: node.element_count,
element_type: "float".to_string(),
is_reduction: node.is_reduction,
reduction_kind: node.reduction_kind,
needs_shuffle: node.needs_reorder,
shuffle_mask: node.reorder_map.clone(),
});
}
}
schedule
}
pub fn generate_slp_code(scheduled: &[ScheduledSLPOp], isa_width_bits: u32) -> Vec<String> {
let mut code = Vec::new();
for op in scheduled {
let vec_type = format!("<{} x {}>", op.element_count, op.element_type);
let inst = match op.opcode {
SLPOpcode::Load => {
format!(
" {} = load {}, ptr %base, align {}",
op.result,
vec_type,
op.element_count * 4 )
}
SLPOpcode::Store => {
format!(
" store {} {}, ptr %base, align {}",
vec_type,
op.operands.first().cloned().unwrap_or_default(),
op.element_count * 4
)
}
SLPOpcode::FAdd | SLPOpcode::Add => {
let (lhs, rhs) = (op.operands.get(0), op.operands.get(1));
format!(
" {} = fadd {} {}, {}",
op.result,
vec_type,
lhs.cloned().unwrap_or_default(),
rhs.cloned().unwrap_or_default()
)
}
SLPOpcode::FMul | SLPOpcode::Mul => {
let (lhs, rhs) = (op.operands.get(0), op.operands.get(1));
format!(
" {} = fmul {} {}, {}",
op.result,
vec_type,
lhs.cloned().unwrap_or_default(),
rhs.cloned().unwrap_or_default()
)
}
SLPOpcode::FSub | SLPOpcode::Sub => {
let (lhs, rhs) = (op.operands.get(0), op.operands.get(1));
format!(
" {} = fsub {} {}, {}",
op.result,
vec_type,
lhs.cloned().unwrap_or_default(),
rhs.cloned().unwrap_or_default()
)
}
_ => {
format!(
" ; {} = {} ({} elements)",
op.result, "vector_op", op.element_count
)
}
};
if op.needs_shuffle {
code.push(format!(" ; shuffle needed for {}", op.result));
}
code.push(inst);
}
code
}
#[derive(Debug, Clone)]
pub struct PartialSLPResult {
pub vectorized_nodes: Vec<usize>,
pub scalar_nodes: Vec<usize>,
pub code: Vec<String>,
pub cost_savings: f64,
}
pub fn prune_unprofitable_subtrees(tree: &mut SLPTree, cost_model: &SLPCostModel) {
let node_ids: Vec<usize> = tree.nodes.keys().copied().collect();
for node_id in node_ids {
if let Some(node) = tree.nodes.get(&node_id) {
if node.is_pruned || !node.is_vectorizable {
continue;
}
let node_cost = cost_model.compute_node_cost(node);
let scalar_cost = match node.opcode {
SLPOpcode::Load => 3.0 * node.element_count as f64,
SLPOpcode::Store => 2.0 * node.element_count as f64,
SLPOpcode::FDiv | SLPOpcode::SDiv | SLPOpcode::UDiv => {
14.0 * node.element_count as f64
}
SLPOpcode::FMul | SLPOpcode::Mul => 4.0 * node.element_count as f64,
_ => 1.0 * node.element_count as f64,
};
let should_prune_cost = node_cost > scalar_cost && node_cost > 0.0;
let should_prune_small = node.element_count < 2;
drop(node);
if should_prune_cost {
tree.prune_subtree(node_id);
}
if should_prune_small {
tree.prune_subtree(node_id);
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SLPVectorizerStats {
pub store_chains_detected: usize,
pub trees_built: usize,
pub trees_vectorized: usize,
pub trees_rejected_cost: usize,
pub trees_rejected_size: usize,
pub nodes_total: usize,
pub nodes_vectorizable: usize,
pub nodes_pruned: usize,
pub reductions_recognized: usize,
pub reorder_opportunities: usize,
pub reorders_applied: usize,
pub patterns_detected: usize,
pub partial_vectorizations: usize,
pub total_slp_instructions: usize,
}
#[derive(Debug, Clone)]
pub struct X86SLPVectorizerDeep {
pub isa_width_bits: u32,
pub cost_model: SLPCostModel,
pub max_tree_depth: usize,
pub min_tree_size: usize,
pub allow_reorder: bool,
pub enable_partial: bool,
pub detect_patterns: bool,
pub stats: SLPVectorizerStats,
}
impl X86SLPVectorizerDeep {
pub fn new(st: &X86Subtarget) -> Self {
let isa_width_bits = if st.has_avx512() {
512
} else if st.has_avx() {
256
} else if st.has_sse2() {
128
} else {
64
};
X86SLPVectorizerDeep {
isa_width_bits,
cost_model: SLPCostModel::new(isa_width_bits),
max_tree_depth: 5,
min_tree_size: 2,
allow_reorder: true,
enable_partial: true,
detect_patterns: true,
stats: SLPVectorizerStats::default(),
}
}
pub fn run_on_block(&mut self, instructions: &[(usize, &str, Vec<&str>)]) -> Vec<String> {
let mut defs: HashMap<String, (SLPOpcode, Vec<String>)> = HashMap::new();
let mut result_counter = 0;
for &(idx, opcode, ref operands) in instructions {
let slp_op = SLPOpcode::from_str(opcode);
let result_name = format!("%r{}", idx);
defs.insert(
result_name.clone(),
(slp_op, operands.iter().map(|s| s.to_string()).collect()),
);
result_counter = result_counter.max(idx + 1);
}
let store_chains = detect_store_chains(instructions);
self.stats.store_chains_detected += store_chains.len();
let mut patterns: Vec<(SLPMultiNodePattern, Vec<String>)> = Vec::new();
if self.detect_patterns {
patterns = detect_slp_patterns(&defs);
self.stats.patterns_detected += patterns.len();
}
let mut all_code: Vec<String> = Vec::new();
for chain in &store_chains {
if chain.stores.len() < self.min_tree_size {
self.stats.trees_rejected_size += 1;
continue;
}
let mut ctx = SLPBuildContext {
defs: defs.clone(),
visited: HashSet::new(),
max_depth: self.max_tree_depth,
current_depth: 0,
allow_reorder: self.allow_reorder,
};
let mut tree = build_slp_tree_bottom_up(chain, &mut ctx);
self.stats.trees_built += 1;
self.stats.nodes_total += tree.nodes.len();
analyze_slp_tree_top_down(&mut tree, self.isa_width_bits);
let vec_nodes = tree.nodes.values().filter(|n| n.is_vectorizable).count();
self.stats.nodes_vectorizable += vec_nodes;
let node_ids: Vec<usize> = tree.nodes.keys().copied().collect();
for node_id in node_ids {
if recognize_horizontal_reduction(&mut tree, node_id).is_some() {
self.stats.reductions_recognized += 1;
}
}
if self.enable_partial {
let before_prune = tree.nodes.values().filter(|n| !n.is_pruned).count();
prune_unprofitable_subtrees(&mut tree, &self.cost_model);
let after_prune = tree.nodes.values().filter(|n| !n.is_pruned).count();
if after_prune < before_prune {
self.stats.partial_vectorizations += 1;
}
self.stats.nodes_pruned += before_prune - after_prune;
}
if !self.cost_model.is_profitable(&tree) {
self.stats.trees_rejected_cost += 1;
continue;
}
let scheduled = schedule_slp_tree(&tree);
let code = generate_slp_code(&scheduled, self.isa_width_bits);
self.stats.total_slp_instructions += code.len();
all_code.extend(code);
self.stats.trees_vectorized += 1;
}
all_code
}
pub fn stats_summary(&self) -> String {
format!(
"SLP Deep: {} chains → {} trees built, {} vectorized, {} rejected (cost), \
{} rejected (size), {} nodes ({} vec, {} pruned), \
{} reductions, {} reorders, {} patterns, {} partial, {} SLP insts",
self.stats.store_chains_detected,
self.stats.trees_built,
self.stats.trees_vectorized,
self.stats.trees_rejected_cost,
self.stats.trees_rejected_size,
self.stats.nodes_total,
self.stats.nodes_vectorizable,
self.stats.nodes_pruned,
self.stats.reductions_recognized,
self.stats.reorder_opportunities,
self.stats.patterns_detected,
self.stats.partial_vectorizations,
self.stats.total_slp_instructions,
)
}
pub fn clear(&mut self) {
self.stats = SLPVectorizerStats::default();
}
}
impl Default for X86SLPVectorizerDeep {
fn default() -> Self {
X86SLPVectorizerDeep {
isa_width_bits: 128,
cost_model: SLPCostModel::new(128),
max_tree_depth: 5,
min_tree_size: 2,
allow_reorder: true,
enable_partial: true,
detect_patterns: true,
stats: SLPVectorizerStats::default(),
}
}
}
pub fn make_x86_slp_deep_sse() -> X86SLPVectorizerDeep {
X86SLPVectorizerDeep {
isa_width_bits: 128,
cost_model: SLPCostModel::new(128),
max_tree_depth: 5,
min_tree_size: 2,
allow_reorder: true,
enable_partial: true,
detect_patterns: true,
stats: SLPVectorizerStats::default(),
}
}
pub fn make_x86_slp_deep_avx() -> X86SLPVectorizerDeep {
X86SLPVectorizerDeep {
isa_width_bits: 256,
cost_model: SLPCostModel::new(256),
max_tree_depth: 6,
min_tree_size: 2,
allow_reorder: true,
enable_partial: true,
detect_patterns: true,
stats: SLPVectorizerStats::default(),
}
}
pub fn make_x86_slp_deep_avx512() -> X86SLPVectorizerDeep {
X86SLPVectorizerDeep {
isa_width_bits: 512,
cost_model: SLPCostModel::new(512),
max_tree_depth: 7,
min_tree_size: 2,
allow_reorder: true,
enable_partial: true,
detect_patterns: true,
stats: SLPVectorizerStats::default(),
}
}
pub fn make_x86_slp_deep_conservative() -> X86SLPVectorizerDeep {
X86SLPVectorizerDeep {
isa_width_bits: 128,
cost_model: {
let mut m = SLPCostModel::new(128);
m.profit_threshold = 0.5; m
},
max_tree_depth: 3,
min_tree_size: 4,
allow_reorder: false,
enable_partial: false,
detect_patterns: false,
stats: SLPVectorizerStats::default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_inst(opcode: &str, operands: Vec<&str>) -> (usize, &str, Vec<&str>) {
(0, opcode, operands)
}
#[test]
fn test_slp_opcode_commutative() {
assert!(SLPOpcode::Add.is_commutative());
assert!(SLPOpcode::FAdd.is_commutative());
assert!(SLPOpcode::Mul.is_commutative());
assert!(SLPOpcode::FMul.is_commutative());
assert!(!SLPOpcode::Sub.is_commutative());
assert!(!SLPOpcode::FSub.is_commutative());
assert!(!SLPOpcode::FDiv.is_commutative());
}
#[test]
fn test_slp_opcode_from_str() {
assert_eq!(SLPOpcode::from_str("add"), SLPOpcode::Add);
assert_eq!(SLPOpcode::from_str("fadd"), SLPOpcode::FAdd);
assert_eq!(SLPOpcode::from_str("load"), SLPOpcode::Load);
assert_eq!(SLPOpcode::from_str("store"), SLPOpcode::Store);
assert_eq!(SLPOpcode::from_str("fmul"), SLPOpcode::FMul);
assert_eq!(SLPOpcode::from_str("unknown_op"), SLPOpcode::Unknown);
}
#[test]
fn test_slp_opcode_is_candidate() {
assert!(SLPOpcode::Add.is_slp_candidate());
assert!(SLPOpcode::Load.is_slp_candidate());
assert!(!SLPOpcode::PHI.is_slp_candidate());
assert!(!SLPOpcode::Unknown.is_slp_candidate());
}
#[test]
fn test_tree_creation() {
let mut tree = SLPTree::new();
let leaf = SLPTreeNode::new_leaf(1, SLPOpcode::Load, vec!["a".into(), "b".into()]);
let id = tree.add_node(leaf);
assert_eq!(id, 1);
assert_eq!(tree.nodes.len(), 1);
assert_eq!(tree.stats.total_nodes, 1);
}
#[test]
fn test_tree_depth_computation() {
let mut tree = SLPTree::new();
let leaf1 = SLPTreeNode::new_leaf(1, SLPOpcode::Load, vec!["a".into(), "b".into()]);
let leaf2 = SLPTreeNode::new_leaf(2, SLPOpcode::Load, vec!["c".into(), "d".into()]);
let leaf1_id = tree.add_node(leaf1);
let leaf2_id = tree.add_node(leaf2);
let root = SLPTreeNode {
id: 3,
opcode: SLPOpcode::FAdd,
lanes: vec![
SLPLane {
value: "x".into(),
operands: vec![],
},
SLPLane {
value: "y".into(),
operands: vec![],
},
],
children: vec![leaf1_id, leaf2_id],
depth: 0,
subtree_depth: 0,
element_count: 2,
is_vectorizable: true,
is_uniform: true,
needs_reorder: false,
reorder_map: None,
vector_width_bits: 0,
cost: 0.0,
is_pruned: false,
is_reduction: false,
reduction_kind: None,
};
tree.root_id = tree.add_node(root);
tree.compute_depths();
let root_node = tree.get_node(tree.root_id).unwrap();
assert_eq!(root_node.depth, 0);
assert_eq!(root_node.subtree_depth, 1);
}
#[test]
fn test_prune_subtree() {
let mut tree = SLPTree::new();
let leaf = SLPTreeNode::new_leaf(1, SLPOpcode::Load, vec!["a".into()]);
let leaf_id = tree.add_node(leaf);
let root = SLPTreeNode {
id: 2,
opcode: SLPOpcode::FAdd,
lanes: vec![SLPLane {
value: "x".into(),
operands: vec![],
}],
children: vec![leaf_id],
depth: 0,
subtree_depth: 0,
element_count: 1,
is_vectorizable: true,
is_uniform: true,
needs_reorder: false,
reorder_map: None,
vector_width_bits: 0,
cost: 0.0,
is_pruned: false,
is_reduction: false,
reduction_kind: None,
};
tree.root_id = tree.add_node(root);
tree.prune_subtree(leaf_id);
assert!(tree.get_node(leaf_id).unwrap().is_pruned);
assert!(tree.stats.pruned_nodes >= 1);
}
#[test]
fn test_bfs_order() {
let mut tree = SLPTree::new();
let leaf = SLPTreeNode::new_leaf(1, SLPOpcode::Load, vec!["a".into()]);
let leaf_id = tree.add_node(leaf);
let root = SLPTreeNode {
id: 2,
opcode: SLPOpcode::FAdd,
lanes: vec![SLPLane {
value: "x".into(),
operands: vec![],
}],
children: vec![leaf_id],
depth: 0,
subtree_depth: 0,
element_count: 1,
is_vectorizable: true,
is_uniform: true,
needs_reorder: false,
reorder_map: None,
vector_width_bits: 0,
cost: 0.0,
is_pruned: false,
is_reduction: false,
reduction_kind: None,
};
tree.root_id = tree.add_node(root);
let order = tree.bfs_iter();
assert_eq!(order.len(), 2);
assert_eq!(order[0], tree.root_id);
}
#[test]
fn test_detect_store_chains_consecutive() {
let insts = vec![
(0usize, "store", vec!["a", "p", "0"]),
(1usize, "store", vec!["b", "p", "4"]),
(2usize, "store", vec!["c", "p", "8"]),
(3usize, "store", vec!["d", "p", "12"]),
];
let chains = detect_store_chains(&insts);
assert_eq!(chains.len(), 1);
assert_eq!(chains[0].stores.len(), 4);
assert!(chains[0].is_consecutive);
assert_eq!(chains[0].stride, 4);
assert_eq!(chains[0].base_ptr, "p");
}
#[test]
fn test_detect_store_chains_non_consecutive() {
let insts = vec![
(0usize, "store", vec!["a", "p", "0"]),
(1usize, "store", vec!["b", "p", "16"]), ];
let chains = detect_store_chains(&insts);
assert_eq!(chains.len(), 1);
assert_eq!(chains[0].stride, 16);
assert!(chains[0].is_consecutive); }
#[test]
fn test_detect_store_chains_different_ptrs() {
let insts = vec![
(0usize, "store", vec!["a", "p", "0"]),
(1usize, "store", vec!["b", "q", "0"]),
];
let chains = detect_store_chains(&insts);
assert!(chains.is_empty());
}
#[test]
fn test_detect_store_chains_empty() {
let insts: Vec<(usize, &str, Vec<&str>)> = vec![];
let chains = detect_store_chains(&insts);
assert!(chains.is_empty());
}
#[test]
fn test_bundle_operands_binary() {
let insts = vec![
(0, SLPOpcode::FAdd, vec!["x0".into(), "y0".into()]),
(1, SLPOpcode::FAdd, vec!["x1".into(), "y1".into()]),
];
let bundles = bundle_operands(&insts);
assert_eq!(bundles.len(), 2); assert_eq!(bundles[0], vec!["x0", "x1"]);
assert_eq!(bundles[1], vec!["y0", "y1"]);
}
#[test]
fn test_bundle_operands_arity_mismatch() {
let insts = vec![
(0, SLPOpcode::FAdd, vec!["x0".into(), "y0".into()]),
(1, SLPOpcode::Call, vec!["x1".into()]), ];
let bundles = bundle_operands(&insts);
assert!(bundles.is_empty());
}
#[test]
fn test_bundle_operands_empty() {
let bundles = bundle_operands(&[]);
assert!(bundles.is_empty());
}
#[test]
fn test_can_bundle_simple() {
let mut seen = HashSet::new();
seen.insert("a".into());
seen.insert("b".into());
assert!(can_bundle(&["a".into(), "b".into()], &seen));
}
#[test]
fn test_can_bundle_single_element() {
let seen = HashSet::new();
assert!(!can_bundle(&["a".into()], &seen));
}
#[test]
fn test_can_bundle_constants() {
let seen = HashSet::new();
assert!(can_bundle(&["const1".into(), "const2".into()], &seen));
}
#[test]
fn test_find_optimal_reorder_identity() {
let a = vec!["x".into(), "y".into()];
let b = vec!["x".into(), "y".into()];
let result = find_optimal_reorder(&a, &b);
assert_eq!(result.permutation, vec![0, 1]);
assert_eq!(result.shuffle_cost, 0);
}
#[test]
fn test_find_optimal_reorder_swapped() {
let a = vec!["x".into(), "y".into()];
let b = vec!["y".into(), "x".into()];
let result = find_optimal_reorder(&a, &b);
assert_eq!(result.permutation, vec![1, 0]);
assert!(result.shuffle_cost > 0);
}
#[test]
fn test_find_optimal_reorder_empty() {
let result = find_optimal_reorder(&[], &[]);
assert!(result.permutation.is_empty());
assert!(!result.is_profitable);
}
#[test]
fn test_find_optimal_reorder_mismatched_lengths() {
let a = vec!["x".into()];
let b = vec!["x".into(), "y".into()];
let result = find_optimal_reorder(&a, &b);
assert!(result.permutation.is_empty());
}
#[test]
fn test_slp_pattern_min_elements() {
assert_eq!(SLPMultiNodePattern::ComplexNumbers.min_elements(), 2);
assert_eq!(SLPMultiNodePattern::RGBAColors.min_elements(), 4);
assert_eq!(SLPMultiNodePattern::Vectors3D.min_elements(), 3);
assert_eq!(SLPMultiNodePattern::Matrix2x2.min_elements(), 4);
assert_eq!(SLPMultiNodePattern::Quaternion.min_elements(), 4);
}
#[test]
fn test_slp_pattern_required_isa() {
assert_eq!(SLPMultiNodePattern::ComplexNumbers.required_isa(), "SSE2");
assert_eq!(SLPMultiNodePattern::Vectors3D.required_isa(), "SSE2");
assert_eq!(SLPMultiNodePattern::StructOfArrays.required_isa(), "SSE");
}
#[test]
fn test_detect_slp_patterns_complex() {
let mut defs = HashMap::new();
defs.insert("a".into(), (SLPOpcode::FAdd, vec!["x".into(), "y".into()]));
defs.insert("b".into(), (SLPOpcode::FSub, vec!["x".into(), "y".into()]));
let patterns = detect_slp_patterns(&defs);
assert!(patterns.len() >= 1);
}
#[test]
fn test_detect_slp_patterns_rgba() {
let mut defs = HashMap::new();
for i in 0..4 {
defs.insert(
format!("r{}", i),
(SLPOpcode::FAdd, vec!["common".into(), format!("v{}", i)]),
);
}
let patterns = detect_slp_patterns(&defs);
let rgba_count = patterns
.iter()
.filter(|(p, _)| *p == SLPMultiNodePattern::RGBAColors)
.count();
assert!(rgba_count >= 1);
}
#[test]
fn test_reduction_kind_sse_instruction() {
assert_eq!(SLPReductionKind::HAdd.sse_instruction(), "haddps");
assert_eq!(SLPReductionKind::DotProduct.sse_instruction(), "dpps");
assert_eq!(SLPReductionKind::SAD.sse_instruction(), "psadbw");
}
#[test]
fn test_reduction_kind_native_support() {
assert!(SLPReductionKind::HAdd.has_native_support());
assert!(SLPReductionKind::DotProduct.has_native_support());
assert!(!SLPReductionKind::HMul.has_native_support());
}
#[test]
fn test_reduction_kind_min_elements() {
assert_eq!(SLPReductionKind::HAdd.min_elements(), 2);
assert_eq!(SLPReductionKind::DotProduct.min_elements(), 4);
assert_eq!(SLPReductionKind::SAD.min_elements(), 8);
}
#[test]
fn test_recognize_horizontal_reduction_add() {
let mut tree = SLPTree::new();
let node = SLPTreeNode {
id: 1,
opcode: SLPOpcode::FAdd,
lanes: vec![
SLPLane {
value: "a".into(),
operands: vec![],
},
SLPLane {
value: "b".into(),
operands: vec![],
},
SLPLane {
value: "c".into(),
operands: vec![],
},
SLPLane {
value: "d".into(),
operands: vec![],
},
],
children: vec![],
depth: 0,
subtree_depth: 0,
element_count: 4,
is_vectorizable: true,
is_uniform: true,
needs_reorder: false,
reorder_map: None,
vector_width_bits: 0,
cost: 0.0,
is_pruned: false,
is_reduction: false,
reduction_kind: None,
};
tree.root_id = tree.add_node(node);
let kind = recognize_horizontal_reduction(&mut tree, tree.root_id);
assert_eq!(kind, Some(SLPReductionKind::HAdd));
let node = tree.get_node(tree.root_id).unwrap();
assert!(node.is_reduction);
}
#[test]
fn test_recognize_horizontal_reduction_dot_product() {
let mut tree = SLPTree::new();
let leaf = SLPTreeNode::new_leaf(
2,
SLPOpcode::Load,
vec!["a".into(), "b".into(), "c".into(), "d".into()],
);
let leaf_id = tree.add_node(leaf);
let root = SLPTreeNode {
id: 1,
opcode: SLPOpcode::FMul,
lanes: vec![
SLPLane {
value: "p0".into(),
operands: vec![],
},
SLPLane {
value: "p1".into(),
operands: vec![],
},
SLPLane {
value: "p2".into(),
operands: vec![],
},
SLPLane {
value: "p3".into(),
operands: vec![],
},
],
children: vec![leaf_id],
depth: 0,
subtree_depth: 0,
element_count: 4,
is_vectorizable: true,
is_uniform: true,
needs_reorder: false,
reorder_map: None,
vector_width_bits: 0,
cost: 0.0,
is_pruned: false,
is_reduction: false,
reduction_kind: None,
};
tree.root_id = tree.add_node(root);
let kind = recognize_horizontal_reduction(&mut tree, tree.root_id);
assert_eq!(kind, Some(SLPReductionKind::DotProduct));
}
#[test]
fn test_cost_model_creation_sse() {
let model = SLPCostModel::new(128);
assert_eq!(model.isa_width_bits, 128);
assert_eq!(model.vector_load_cost, 3.5);
}
#[test]
fn test_cost_model_avx512() {
let model = SLPCostModel::new(512);
assert_eq!(model.vector_load_cost, 5.0);
assert_eq!(model.vector_arith_cost, 1.0);
}
#[test]
fn test_compute_node_cost_load() {
let model = SLPCostModel::new(128);
let node = SLPTreeNode::new_leaf(1, SLPOpcode::Load, vec!["a".into(), "b".into()]);
let cost = model.compute_node_cost(&node);
assert_eq!(cost, model.vector_load_cost);
}
#[test]
fn test_compute_node_cost_div() {
let model = SLPCostModel::new(128);
let node = SLPTreeNode::new_leaf(1, SLPOpcode::FDiv, vec!["a".into(), "b".into()]);
let cost = model.compute_node_cost(&node);
assert_eq!(cost, model.vector_div_cost);
}
#[test]
fn test_compute_tree_cost() {
let mut tree = SLPTree::new();
let leaf = SLPTreeNode::new_leaf(1, SLPOpcode::Load, vec!["a".into(), "b".into()]);
let leaf_id = tree.add_node(leaf);
let root = SLPTreeNode {
id: 2,
opcode: SLPOpcode::FAdd,
lanes: vec![
SLPLane {
value: "x".into(),
operands: vec![],
},
SLPLane {
value: "y".into(),
operands: vec![],
},
],
children: vec![leaf_id],
depth: 0,
subtree_depth: 0,
element_count: 2,
is_vectorizable: true,
is_uniform: true,
needs_reorder: false,
reorder_map: None,
vector_width_bits: 0,
cost: 0.0,
is_pruned: false,
is_reduction: false,
reduction_kind: None,
};
tree.root_id = tree.add_node(root);
let model = SLPCostModel::new(128);
let cost = model.compute_tree_cost(&tree);
assert!(cost > 0.0);
}
#[test]
fn test_is_profitable() {
let mut tree = SLPTree::new();
let leaf1 = SLPTreeNode::new_leaf(
1,
SLPOpcode::Load,
vec!["a".into(), "b".into(), "c".into(), "d".into()],
);
let leaf2 = SLPTreeNode::new_leaf(
2,
SLPOpcode::Load,
vec!["e".into(), "f".into(), "g".into(), "h".into()],
);
let leaf1_id = tree.add_node(leaf1);
let leaf2_id = tree.add_node(leaf2);
let root = SLPTreeNode {
id: 3,
opcode: SLPOpcode::FAdd,
lanes: vec![
SLPLane {
value: "r0".into(),
operands: vec![],
},
SLPLane {
value: "r1".into(),
operands: vec![],
},
SLPLane {
value: "r2".into(),
operands: vec![],
},
SLPLane {
value: "r3".into(),
operands: vec![],
},
],
children: vec![leaf1_id, leaf2_id],
depth: 0,
subtree_depth: 0,
element_count: 4,
is_vectorizable: true,
is_uniform: true,
needs_reorder: false,
reorder_map: None,
vector_width_bits: 0,
cost: 0.0,
is_pruned: false,
is_reduction: false,
reduction_kind: None,
};
tree.root_id = tree.add_node(root);
let model = SLPCostModel::new(128);
assert!(model.is_profitable(&tree));
}
#[test]
fn test_schedule_slp_tree() {
let mut tree = SLPTree::new();
let leaf1 = SLPTreeNode::new_leaf(1, SLPOpcode::Load, vec!["a".into(), "b".into()]);
let leaf2 = SLPTreeNode::new_leaf(2, SLPOpcode::Load, vec!["c".into(), "d".into()]);
let leaf1_id = tree.add_node(leaf1);
let leaf2_id = tree.add_node(leaf2);
let root = SLPTreeNode {
id: 3,
opcode: SLPOpcode::FAdd,
lanes: vec![
SLPLane {
value: "x".into(),
operands: vec![],
},
SLPLane {
value: "y".into(),
operands: vec![],
},
],
children: vec![leaf1_id, leaf2_id],
depth: 0,
subtree_depth: 0,
element_count: 2,
is_vectorizable: true,
is_uniform: true,
needs_reorder: false,
reorder_map: None,
vector_width_bits: 0,
cost: 0.0,
is_pruned: false,
is_reduction: false,
reduction_kind: None,
};
tree.root_id = tree.add_node(root);
tree.compute_depths();
let scheduled = schedule_slp_tree(&tree);
assert!(scheduled.len() >= 3);
}
#[test]
fn test_generate_slp_code() {
let ops = vec![
ScheduledSLPOp {
opcode: SLPOpcode::Load,
result: "v0".into(),
operands: vec![],
element_count: 4,
element_type: "float".into(),
is_reduction: false,
reduction_kind: None,
needs_shuffle: false,
shuffle_mask: None,
},
ScheduledSLPOp {
opcode: SLPOpcode::FAdd,
result: "v1".into(),
operands: vec!["v0".into(), "v_other".into()],
element_count: 4,
element_type: "float".into(),
is_reduction: false,
reduction_kind: None,
needs_shuffle: false,
shuffle_mask: None,
},
];
let code = generate_slp_code(&ops, 128);
assert!(code.len() >= 2);
assert!(code[0].contains("load"));
assert!(code[1].contains("fadd"));
}
#[test]
fn test_prune_unprofitable_subtrees() {
let mut tree = SLPTree::new();
let node = SLPTreeNode::new_leaf(1, SLPOpcode::FDiv, vec!["a".into()]);
tree.root_id = tree.add_node(node);
let model = SLPCostModel::new(128);
let before = tree.nodes.values().filter(|n| !n.is_pruned).count();
prune_unprofitable_subtrees(&mut tree, &model);
let after = tree.nodes.values().filter(|n| !n.is_pruned).count();
assert!(after <= before);
}
#[test]
fn test_slp_vectorizer_new_sse() {
let mut st = X86Subtarget::default();
st.set_sse2(true);
let slp = X86SLPVectorizerDeep::new(&st);
assert_eq!(slp.isa_width_bits, 128);
}
#[test]
fn test_slp_vectorizer_run_simple() {
let insts = vec![
(0usize, "store", vec!["r1", "p", "0"]),
(1usize, "store", vec!["r2", "p", "4"]),
(2usize, "fadd", vec!["r1", "a0", "b0"]),
(3usize, "fadd", vec!["r2", "a1", "b1"]),
(4usize, "load", vec!["a0", "pa", "0"]),
(5usize, "load", vec!["a1", "pa", "4"]),
(6usize, "load", vec!["b0", "pb", "0"]),
(7usize, "load", vec!["b1", "pb", "4"]),
];
let mut st = X86Subtarget::default();
st.set_sse2(true);
let mut slp = X86SLPVectorizerDeep::new(&st);
let code = slp.run_on_block(&insts);
assert!(slp.stats.store_chains_detected >= 1);
}
#[test]
fn test_slp_stats_summary() {
let mut slp = make_x86_slp_deep_sse();
slp.stats.store_chains_detected = 5;
slp.stats.trees_vectorized = 3;
slp.stats.trees_rejected_cost = 2;
let summary = slp.stats_summary();
assert!(summary.contains("5"));
assert!(summary.contains("3"));
assert!(summary.contains("2"));
}
#[test]
fn test_slp_clear() {
let mut slp = make_x86_slp_deep_sse();
slp.stats.store_chains_detected = 10;
slp.stats.trees_vectorized = 5;
slp.clear();
assert_eq!(slp.stats.store_chains_detected, 0);
assert_eq!(slp.stats.trees_vectorized, 0);
}
#[test]
fn test_make_sse() {
let slp = make_x86_slp_deep_sse();
assert_eq!(slp.isa_width_bits, 128);
assert_eq!(slp.max_tree_depth, 5);
}
#[test]
fn test_make_avx() {
let slp = make_x86_slp_deep_avx();
assert_eq!(slp.isa_width_bits, 256);
assert_eq!(slp.max_tree_depth, 6);
}
#[test]
fn test_make_avx512() {
let slp = make_x86_slp_deep_avx512();
assert_eq!(slp.isa_width_bits, 512);
assert_eq!(slp.max_tree_depth, 7);
}
#[test]
fn test_make_conservative() {
let slp = make_x86_slp_deep_conservative();
assert!(!slp.allow_reorder);
assert!(!slp.enable_partial);
assert!(!slp.detect_patterns);
assert_eq!(slp.min_tree_size, 4);
}
#[test]
fn test_default() {
let slp = X86SLPVectorizerDeep::default();
assert_eq!(slp.isa_width_bits, 128);
assert!(slp.allow_reorder);
assert!(slp.enable_partial);
}
#[test]
fn test_empty_block() {
let insts: Vec<(usize, &str, Vec<&str>)> = vec![];
let mut st = X86Subtarget::default();
st.set_sse2(true);
let mut slp = X86SLPVectorizerDeep::new(&st);
let code = slp.run_on_block(&insts);
assert!(code.is_empty());
}
#[test]
fn test_no_stores() {
let insts = vec![
(0usize, "fadd", vec!["r1", "a0", "b0"]),
(1usize, "fadd", vec!["r2", "a1", "b1"]),
];
let mut st = X86Subtarget::default();
st.set_sse2(true);
let mut slp = X86SLPVectorizerDeep::new(&st);
let code = slp.run_on_block(&insts);
assert_eq!(slp.stats.store_chains_detected, 0);
}
#[test]
fn test_single_store() {
let insts = vec![(0usize, "store", vec!["r1", "p", "0"])];
let mut st = X86Subtarget::default();
st.set_sse2(true);
let mut slp = X86SLPVectorizerDeep::new(&st);
let code = slp.run_on_block(&insts);
assert_eq!(slp.stats.trees_rejected_size, 1);
}
#[test]
fn test_slp_node_is_leaf() {
let leaf = SLPTreeNode::new_leaf(1, SLPOpcode::Load, vec!["a".into()]);
assert!(leaf.is_leaf());
let internal = SLPTreeNode {
id: 2,
opcode: SLPOpcode::FAdd,
lanes: vec![SLPLane {
value: "x".into(),
operands: vec![],
}],
children: vec![1],
depth: 0,
subtree_depth: 0,
element_count: 1,
is_vectorizable: true,
is_uniform: true,
needs_reorder: false,
reorder_map: None,
vector_width_bits: 0,
cost: 0.0,
is_pruned: false,
is_reduction: false,
reduction_kind: None,
};
assert!(!internal.is_leaf());
}
#[test]
fn test_reorder_not_profitable_for_small_n() {
let a = vec!["x".into()];
let b = vec!["y".into()];
let result = find_optimal_reorder(&a, &b);
assert!(!result.is_profitable);
}
#[test]
fn test_cost_model_profit_threshold() {
let model = SLPCostModel::new(128);
assert!(model.profit_threshold < 1.0);
assert!(model.profit_threshold > 0.0);
}
#[test]
fn test_tree_stats_default() {
let stats = SLPTreeStats::default();
assert_eq!(stats.total_nodes, 0);
assert_eq!(stats.max_depth, 0);
}
#[test]
fn test_analyze_slp_tree_top_down() {
let mut tree = SLPTree::new();
let leaf = SLPTreeNode::new_leaf(
1,
SLPOpcode::Load,
vec!["a".into(), "b".into(), "c".into(), "d".into()],
);
tree.root_id = tree.add_node(leaf);
analyze_slp_tree_top_down(&mut tree, 256);
let node = tree.get_node(tree.root_id).unwrap();
assert_eq!(node.vector_width_bits, 128);
}
#[test]
fn test_compute_scalar_cost() {
let mut tree = SLPTree::new();
let leaf = SLPTreeNode::new_leaf(1, SLPOpcode::Load, vec!["a".into(), "b".into()]);
tree.root_id = tree.add_node(leaf);
let model = SLPCostModel::new(128);
let scalar_cost = model.compute_scalar_cost(&tree);
assert!(scalar_cost > 0.0);
}
#[test]
fn test_slp_vectorizer_stats_default() {
let stats = SLPVectorizerStats::default();
assert_eq!(stats.store_chains_detected, 0);
assert_eq!(stats.trees_vectorized, 0);
}
#[test]
fn test_no_panic_on_empty_tree_cost() {
let tree = SLPTree::new();
let model = SLPCostModel::new(128);
let cost = model.compute_tree_cost(&tree);
assert_eq!(cost, 0.0);
}
#[test]
fn test_slp_opcode_all_from_str_coverage() {
let opcodes = [
"add",
"sub",
"mul",
"udiv",
"sdiv",
"and",
"or",
"xor",
"fadd",
"fsub",
"fmul",
"fdiv",
"icmp",
"fcmp",
"select",
"trunc",
"zext",
"sext",
"fptrunc",
"fpext",
"fptoui",
"fptosi",
"uitofp",
"sitofp",
"bitcast",
"phi",
"call",
"extractelement",
"insertelement",
"shufflevector",
"load",
"store",
"ashr",
"lshr",
"shl",
];
for &o in &opcodes {
let parsed = SLPOpcode::from_str(o);
assert_ne!(parsed, SLPOpcode::Unknown, "Failed to parse: {}", o);
}
}
}