use crate::analysis::{DominatorTree, LoopAnalysis, LoopInfo};
use crate::opcode::{ICmpPred, Opcode};
use crate::scalar_evolution::{ScalarEvolution, SCEV};
use crate::types::Type;
use crate::value::{valref, SubclassKind, ValueRef};
use crate::x86::x86_instr_info::{X86InstrInfo, X86Opcode};
use crate::x86::x86_schedule_model::{
ice_lake_model, skylake_client_model, zen3_model, zen4_model, zen5_model, SchedMachineModel,
SchedModel,
};
use crate::x86::x86_subtarget::X86Subtarget;
use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque};
pub use crate::opcode::Opcode as LoopOpcode;
pub type BlockId = u64;
pub type ValueId = u64;
pub type LoopDepth = u32;
pub type InstCount = usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum X86MicroArch {
Core2,
Nehalem,
SandyBridge,
Haswell,
Skylake,
IceLake,
AlderLakeP,
AlderLakeE,
GraniteRapids,
K8,
Bulldozer,
Zen1,
Zen2,
Zen3,
Zen4,
Zen5,
Generic,
}
impl X86MicroArch {
pub fn has_lsd(&self) -> bool {
matches!(
self,
X86MicroArch::SandyBridge
| X86MicroArch::Haswell
| X86MicroArch::Skylake
| X86MicroArch::IceLake
)
}
pub fn has_uop_cache(&self) -> bool {
matches!(
self,
X86MicroArch::SandyBridge
| X86MicroArch::Haswell
| X86MicroArch::Skylake
| X86MicroArch::IceLake
| X86MicroArch::AlderLakeP
| X86MicroArch::AlderLakeE
| X86MicroArch::GraniteRapids
| X86MicroArch::Zen1
| X86MicroArch::Zen2
| X86MicroArch::Zen3
| X86MicroArch::Zen4
| X86MicroArch::Zen5
)
}
pub fn uop_cache_size(&self) -> usize {
match self {
X86MicroArch::SandyBridge | X86MicroArch::Haswell => 1536,
X86MicroArch::Skylake | X86MicroArch::IceLake => 1536,
X86MicroArch::AlderLakeP => 4096,
X86MicroArch::AlderLakeE => 2048,
X86MicroArch::GraniteRapids => 4096,
X86MicroArch::Zen1 | X86MicroArch::Zen2 => 4096,
X86MicroArch::Zen3 | X86MicroArch::Zen4 => 4096,
X86MicroArch::Zen5 => 6750,
_ => 1024,
}
}
pub fn lsd_issue_width(&self) -> usize {
match self {
X86MicroArch::SandyBridge | X86MicroArch::Haswell => 4,
X86MicroArch::Skylake | X86MicroArch::IceLake => 4,
X86MicroArch::AlderLakeP => 6,
_ => 4,
}
}
pub fn decode_width(&self) -> usize {
match self {
X86MicroArch::Core2 | X86MicroArch::Nehalem => 4,
X86MicroArch::SandyBridge | X86MicroArch::Haswell | X86MicroArch::Skylake => 4,
X86MicroArch::IceLake => 5,
X86MicroArch::AlderLakeP | X86MicroArch::GraniteRapids => 6,
X86MicroArch::AlderLakeE => 3,
X86MicroArch::Zen1 | X86MicroArch::Zen2 | X86MicroArch::Zen3 | X86MicroArch::Zen4 => 4,
X86MicroArch::Zen5 => 8,
_ => 4,
}
}
pub fn btb_entries(&self) -> usize {
match self {
X86MicroArch::SandyBridge | X86MicroArch::Haswell => 4096,
X86MicroArch::Skylake | X86MicroArch::IceLake => 5120,
X86MicroArch::AlderLakeP | X86MicroArch::GraniteRapids => 12288,
X86MicroArch::Zen3 | X86MicroArch::Zen4 => 1024, X86MicroArch::Zen5 => 1536,
_ => 4096,
}
}
pub fn preferred_loop_alignment(&self) -> u32 {
match self {
X86MicroArch::SandyBridge | X86MicroArch::Haswell | X86MicroArch::Skylake => 16,
X86MicroArch::IceLake | X86MicroArch::AlderLakeP | X86MicroArch::GraniteRapids => 32,
X86MicroArch::Zen1 | X86MicroArch::Zen2 | X86MicroArch::Zen3 | X86MicroArch::Zen4 => 32,
X86MicroArch::Zen5 => 64,
_ => 16,
}
}
}
#[derive(Debug, Clone)]
pub struct X86NaturalLoop {
pub id: u64,
pub header: BlockId,
pub blocks: Vec<BlockId>,
pub preheader: Option<BlockId>,
pub latches: Vec<BlockId>,
pub exiting_blocks: Vec<BlockId>,
pub exit_blocks: Vec<BlockId>,
pub exit_edges: Vec<(BlockId, BlockId)>,
pub back_edges: Vec<(BlockId, BlockId)>,
pub depth: LoopDepth,
pub parent: Option<u64>,
pub children: Vec<u64>,
pub is_reducible: bool,
pub trip_count: TripCountEstimate,
pub body_size: usize,
pub uop_count: usize,
pub contains_calls: bool,
pub contains_memory_ops: bool,
pub is_vectorizable: bool,
pub invariants: Vec<ValueId>,
pub induction_vars: Vec<InductionVariable>,
pub is_canonical: bool,
pub canonical_latch: Option<BlockId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TripCountEstimate {
Exact(u64),
Max(u64),
Symbolic(String),
Unknown,
}
impl TripCountEstimate {
pub fn as_exact(&self) -> Option<u64> {
match self {
TripCountEstimate::Exact(n) => Some(*n),
_ => None,
}
}
pub fn as_bound(&self) -> Option<u64> {
match self {
TripCountEstimate::Exact(n) => Some(*n),
TripCountEstimate::Max(n) => Some(*n),
_ => None,
}
}
pub fn is_exact(&self) -> bool {
matches!(self, TripCountEstimate::Exact(_))
}
pub fn has_bound(&self) -> bool {
matches!(
self,
TripCountEstimate::Exact(_) | TripCountEstimate::Max(_)
)
}
}
impl std::fmt::Display for TripCountEstimate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TripCountEstimate::Exact(n) => write!(f, "exact({})", n),
TripCountEstimate::Max(n) => write!(f, "max({})", n),
TripCountEstimate::Symbolic(s) => write!(f, "symbolic({})", s),
TripCountEstimate::Unknown => write!(f, "unknown"),
}
}
}
#[derive(Debug, Clone)]
pub struct InductionVariable {
pub value: ValueId,
pub start: i64,
pub step: i64,
pub base_iv: Option<ValueId>,
pub is_basic: bool,
pub scev: Option<SCEV>,
pub loop_id: u64,
pub bit_width: u32,
pub is_signed: bool,
pub is_loop_only: bool,
}
impl InductionVariable {
pub fn new_basic(value: ValueId, start: i64, step: i64, loop_id: u64, bit_width: u32) -> Self {
Self {
value,
start,
step,
base_iv: None,
is_basic: true,
scev: None,
loop_id,
bit_width,
is_signed: true,
is_loop_only: false,
}
}
pub fn new_derived(
value: ValueId,
base_iv: ValueId,
multiplier: i64,
addend: i64,
loop_id: u64,
bit_width: u32,
) -> Self {
Self {
value,
start: addend,
step: multiplier,
base_iv: Some(base_iv),
is_basic: false,
scev: None,
loop_id,
bit_width,
is_signed: true,
is_loop_only: false,
}
}
pub fn at_iteration(&self, iter: i64) -> i64 {
self.start + self.step * iter
}
pub fn is_integer_iv(&self) -> bool {
true
}
}
pub struct X86LoopOptimizer {
pub subtarget: X86Subtarget,
pub microarch: X86MicroArch,
pub loops: Vec<X86NaturalLoop>,
pub block_to_loop: HashMap<BlockId, u64>,
pub dom_tree: Option<DominatorTree>,
pub scev: Option<ScalarEvolution>,
pub instr_info: X86InstrInfo,
pub loops_analyzed: usize,
pub loops_transformed: usize,
pub debug_trace: bool,
pub stats: X86LoopOptStats,
pub cost_config: X86LoopCostConfig,
}
#[derive(Debug, Clone, Default)]
pub struct X86LoopOptStats {
pub loops_rotated: usize,
pub fully_unrolled: usize,
pub partially_unrolled: usize,
pub unroll_and_jammed: usize,
pub fused: usize,
pub distributed: usize,
pub interchanged: usize,
pub unswitched: usize,
pub idioms_recognized: usize,
pub deleted: usize,
pub simplified: usize,
pub strength_reduced: usize,
pub rerolled: usize,
pub versioned: usize,
pub predicated: usize,
pub prefetches_inserted: usize,
pub headers_aligned: usize,
pub nop_bytes_padded: usize,
pub ivs_optimized: usize,
pub invariants_hoisted: usize,
}
impl X86LoopOptStats {
pub fn new() -> Self {
Self::default()
}
pub fn made_progress(&self) -> bool {
self.loops_rotated > 0
|| self.fully_unrolled > 0
|| self.partially_unrolled > 0
|| self.unroll_and_jammed > 0
|| self.fused > 0
|| self.distributed > 0
|| self.interchanged > 0
|| self.unswitched > 0
|| self.idioms_recognized > 0
|| self.deleted > 0
|| self.simplified > 0
|| self.strength_reduced > 0
|| self.rerolled > 0
|| self.versioned > 0
|| self.predicated > 0
|| self.ivs_optimized > 0
}
pub fn merge(&mut self, other: &X86LoopOptStats) {
self.loops_rotated += other.loops_rotated;
self.fully_unrolled += other.fully_unrolled;
self.partially_unrolled += other.partially_unrolled;
self.unroll_and_jammed += other.unroll_and_jammed;
self.fused += other.fused;
self.distributed += other.distributed;
self.interchanged += other.interchanged;
self.unswitched += other.unswitched;
self.idioms_recognized += other.idioms_recognized;
self.deleted += other.deleted;
self.simplified += other.simplified;
self.strength_reduced += other.strength_reduced;
self.rerolled += other.rerolled;
self.versioned += other.versioned;
self.predicated += other.predicated;
self.prefetches_inserted += other.prefetches_inserted;
self.headers_aligned += other.headers_aligned;
self.nop_bytes_padded += other.nop_bytes_padded;
self.ivs_optimized += other.ivs_optimized;
self.invariants_hoisted += other.invariants_hoisted;
}
}
#[derive(Debug, Clone)]
pub struct X86LoopCostConfig {
pub max_unroll_factor: u32,
pub max_full_unroll_insts: usize,
pub max_partial_unroll_insts: usize,
pub max_unroll_jam_factor: u32,
pub max_predication_body_size: usize,
pub min_trip_count_for_unroll: u64,
pub max_trip_count_full_unroll: u64,
pub min_body_size_strength_reduce: usize,
pub max_loop_depth_interchange: u32,
pub loop_alignment: u32,
pub align_loop_headers: bool,
pub insert_prefetches: bool,
pub l1_cache_line_size: u32,
pub l2_cache_line_size: u32,
pub uop_cache_capacity: usize,
pub lsd_available: bool,
pub lsd_capacity: usize,
}
impl Default for X86LoopCostConfig {
fn default() -> Self {
Self {
max_unroll_factor: 8,
max_full_unroll_insts: 200,
max_partial_unroll_insts: 100,
max_unroll_jam_factor: 4,
max_predication_body_size: 20,
min_trip_count_for_unroll: 4,
max_trip_count_full_unroll: 256,
min_body_size_strength_reduce: 3,
max_loop_depth_interchange: 3,
loop_alignment: 16,
align_loop_headers: true,
insert_prefetches: true,
l1_cache_line_size: 64,
l2_cache_line_size: 64,
uop_cache_capacity: 1536,
lsd_available: true,
lsd_capacity: 288,
}
}
}
impl X86LoopCostConfig {
pub fn for_microarch(microarch: X86MicroArch) -> Self {
let base = Self::default();
match microarch {
X86MicroArch::Skylake => Self {
loop_alignment: 16,
uop_cache_capacity: 1536,
lsd_capacity: 288,
lsd_available: true,
max_unroll_factor: 8,
..base
},
X86MicroArch::IceLake => Self {
loop_alignment: 32,
uop_cache_capacity: 2304,
lsd_capacity: 384,
lsd_available: true,
max_unroll_factor: 8,
..base
},
X86MicroArch::AlderLakeP => Self {
loop_alignment: 32,
uop_cache_capacity: 4096,
lsd_capacity: 512,
lsd_available: false, max_unroll_factor: 10,
..base
},
X86MicroArch::Zen3 => Self {
loop_alignment: 32,
uop_cache_capacity: 4096,
lsd_available: false,
max_unroll_factor: 8,
..base
},
X86MicroArch::Zen4 => Self {
loop_alignment: 32,
uop_cache_capacity: 6750,
lsd_available: false,
max_unroll_factor: 8,
..base
},
X86MicroArch::Zen5 => Self {
loop_alignment: 64,
uop_cache_capacity: 6750,
lsd_available: false,
max_unroll_factor: 10,
max_unroll_jam_factor: 8,
..base
},
_ => base,
}
}
pub fn should_partial_unroll(
&self,
trip_count: &TripCountEstimate,
body_uops: usize,
contains_calls: bool,
) -> bool {
if contains_calls {
return false;
}
let min_trips = match trip_count.as_bound() {
Some(n) if n >= self.min_trip_count_for_unroll => true,
_ => false,
};
if !min_trips {
return false;
}
body_uops <= self.uop_cache_capacity / 2
}
pub fn should_full_unroll(
&self,
trip_count: &TripCountEstimate,
body_insts: usize,
contains_calls: bool,
) -> bool {
if contains_calls {
return false;
}
let exact = match trip_count.as_exact() {
Some(n) if n <= self.max_trip_count_full_unroll => n,
_ => return false,
};
(body_insts * exact as usize) <= self.max_full_unroll_insts
}
pub fn compute_unroll_factor(&self, trip_count: &TripCountEstimate, body_uops: usize) -> u32 {
let bound = match trip_count.as_bound() {
Some(n) => n,
None => return 1,
};
let max_by_cache = if body_uops > 0 {
(self.uop_cache_capacity / body_uops).min(self.max_unroll_factor as usize)
} else {
self.max_unroll_factor as usize
};
let mut factor = 1u32;
for candidate in (2..=max_by_cache.min(self.max_unroll_factor as usize)).rev() {
if bound % candidate as u64 == 0 || (bound / candidate as u64) >= 2 {
factor = candidate as u32;
break;
}
}
factor.max(1).min(self.max_unroll_factor)
}
}
impl X86LoopOptimizer {
pub fn new(subtarget: X86Subtarget) -> Self {
let microarch = Self::detect_microarch(&subtarget);
let cost_config = X86LoopCostConfig::for_microarch(microarch);
Self {
subtarget,
microarch,
loops: Vec::new(),
block_to_loop: HashMap::new(),
dom_tree: None,
scev: None,
instr_info: X86InstrInfo::new(),
loops_analyzed: 0,
loops_transformed: 0,
debug_trace: false,
stats: X86LoopOptStats::new(),
cost_config,
}
}
pub fn with_cost_config(subtarget: X86Subtarget, cost_config: X86LoopCostConfig) -> Self {
let microarch = Self::detect_microarch(&subtarget);
Self {
subtarget,
microarch,
loops: Vec::new(),
block_to_loop: HashMap::new(),
dom_tree: None,
scev: None,
instr_info: X86InstrInfo::new(),
loops_analyzed: 0,
loops_transformed: 0,
debug_trace: false,
stats: X86LoopOptStats::new(),
cost_config,
}
}
fn detect_microarch(subtarget: &X86Subtarget) -> X86MicroArch {
let cpu = subtarget.cpu.to_lowercase();
match cpu.as_str() {
"core2" | "penryn" => X86MicroArch::Core2,
"nehalem" | "westmere" => X86MicroArch::Nehalem,
"sandybridge" | "ivybridge" => X86MicroArch::SandyBridge,
"haswell" | "broadwell" => X86MicroArch::Haswell,
"skylake" | "kabylake" | "coffeelake" | "cometlake" | "cascadelake" | "cooperlake" => {
X86MicroArch::Skylake
}
"icelake" | "tigerlake" | "rocketlake" => X86MicroArch::IceLake,
"alderlake" | "raptorlake" => X86MicroArch::AlderLakeP,
"graniterapids" | "sierraforest" => X86MicroArch::GraniteRapids,
"znver1" => X86MicroArch::Zen1,
"znver2" => X86MicroArch::Zen2,
"znver3" => X86MicroArch::Zen3,
"znver4" => X86MicroArch::Zen4,
"znver5" => X86MicroArch::Zen5,
"bdver1" | "bdver2" | "bdver3" | "bdver4" => X86MicroArch::Bulldozer,
_ => X86MicroArch::Generic,
}
}
pub fn run_pipeline(
&mut self,
func: &ValueRef,
blocks: &HashMap<BlockId, Vec<ValueId>>,
pred_map: &HashMap<BlockId, Vec<BlockId>>,
succ_map: &HashMap<BlockId, Vec<BlockId>>,
) -> &X86LoopOptStats {
self.reset();
self.detect_loops(func, blocks, pred_map, succ_map);
self.run_loop_simplify();
self.run_idiom_recognition();
self.run_loop_deletion();
self.compute_scev_for_loops(func);
self.analyze_induction_vars();
self.run_loop_rotation();
self.run_loop_interchange();
self.run_loop_unswitching();
self.run_loop_distribution();
self.run_loop_fusion();
self.run_strength_reduction();
self.run_loop_unrolling();
self.run_unroll_and_jam();
self.run_loop_versioning();
self.run_loop_predication();
self.run_loop_rerolling();
self.run_x86_tuning();
self.loops_transformed = self.count_transformations();
&self.stats
}
fn reset(&mut self) {
self.loops.clear();
self.block_to_loop.clear();
self.dom_tree = None;
self.loops_analyzed = 0;
self.loops_transformed = 0;
self.stats = X86LoopOptStats::new();
}
fn count_transformations(&self) -> usize {
self.stats.loops_rotated
+ self.stats.fully_unrolled
+ self.stats.partially_unrolled
+ self.stats.unroll_and_jammed
+ self.stats.fused
+ self.stats.distributed
+ self.stats.interchanged
+ self.stats.unswitched
+ self.stats.idioms_recognized
+ self.stats.deleted
+ self.stats.simplified
+ self.stats.strength_reduced
+ self.stats.rerolled
+ self.stats.versioned
+ self.stats.predicated
}
pub fn detect_loops(
&mut self,
func: &ValueRef,
blocks: &HashMap<BlockId, Vec<ValueId>>,
pred_map: &HashMap<BlockId, Vec<BlockId>>,
succ_map: &HashMap<BlockId, Vec<BlockId>>,
) -> &[X86NaturalLoop] {
if blocks.is_empty() {
return &self.loops;
}
let dom_tree = self.build_dominator_tree(blocks, pred_map);
self.dom_tree = Some(dom_tree.clone());
let backedges = self.find_back_edges(blocks, succ_map, &dom_tree);
let mut raw_loops: Vec<(BlockId, Vec<BlockId>, BlockId, Vec<(BlockId, BlockId)>)> =
Vec::new();
for (src, tgt) in &backedges {
let header = *tgt;
let body = self.discover_loop_body(header, *src, blocks, succ_map);
let back_edges_list = self.collect_backedges_for_header(header, &backedges);
raw_loops.push((header, body, *src, back_edges_list));
}
let mut header_to_loops: HashMap<BlockId, Vec<usize>> = HashMap::new();
for (idx, (header, _, _, _)) in raw_loops.iter().enumerate() {
header_to_loops.entry(*header).or_default().push(idx);
}
let mut next_id: u64 = 0;
for (header, indices) in &header_to_loops {
let mut all_blocks: Vec<BlockId> = Vec::new();
let mut all_latches: Vec<BlockId> = Vec::new();
let mut all_back_edges: Vec<(BlockId, BlockId)> = Vec::new();
for &idx in indices {
let (_, ref body, latch, ref back_edges) = raw_loops[idx];
for b in body {
if !all_blocks.contains(b) {
all_blocks.push(*b);
}
}
if !all_latches.contains(&latch) {
all_latches.push(latch);
}
for be in back_edges {
if !all_back_edges.contains(be) {
all_back_edges.push(*be);
}
}
}
let (exiting, exit_blocks, exit_edges) =
self.compute_exit_info(*header, &all_blocks, succ_map);
let is_reducible = self.is_loop_reducible(*header, &all_blocks, pred_map);
let preheader = self.find_preheader(*header, &all_blocks, pred_map);
let trip_count = self.estimate_trip_count(*header, &all_blocks);
let body_size = self.estimate_body_size(&all_blocks, blocks);
let uop_count = self.estimate_uop_count(&all_blocks, blocks);
let contains_calls = self.loop_contains_calls(&all_blocks, blocks);
let contains_memory_ops = self.loop_contains_memory_ops(&all_blocks, blocks);
let loop_info = X86NaturalLoop {
id: next_id,
header: *header,
blocks: all_blocks.clone(),
preheader,
latches: all_latches,
exiting_blocks: exiting,
exit_blocks,
exit_edges,
back_edges: all_back_edges,
depth: 0, parent: None,
children: Vec::new(),
is_reducible,
trip_count,
body_size,
uop_count,
contains_calls,
contains_memory_ops,
is_vectorizable: false,
invariants: Vec::new(),
induction_vars: Vec::new(),
is_canonical: false,
canonical_latch: None,
};
self.block_to_loop.insert(*header, next_id);
for b in &loop_info.blocks {
self.block_to_loop.entry(*b).or_insert(next_id);
}
self.loops.push(loop_info);
next_id += 1;
}
self.compute_loop_nesting();
self.loops_analyzed = self.loops.len();
&self.loops
}
fn build_dominator_tree(
&self,
blocks: &HashMap<BlockId, Vec<ValueId>>,
pred_map: &HashMap<BlockId, Vec<BlockId>>,
) -> DominatorTree {
let entries: Vec<BlockId> = blocks.keys().copied().collect();
if entries.is_empty() {
return DominatorTree {
idom: HashMap::new(),
children: HashMap::new(),
dom_level: HashMap::new(),
dfs_in: HashMap::new(),
dfs_out: HashMap::new(),
dfs_order: Vec::new(),
};
}
let entry = entries
.iter()
.find(|b| pred_map.get(b).map_or(true, |p| p.is_empty()))
.copied()
.unwrap_or(entries[0]);
let mut dfs_order: Vec<BlockId> = Vec::new();
let mut dfs_visited: HashSet<BlockId> = HashSet::new();
self.dfs_block(entry, blocks, pred_map, &mut dfs_order, &mut dfs_visited);
let mut dfs_idx: HashMap<BlockId, usize> = HashMap::new();
for (i, b) in dfs_order.iter().enumerate() {
dfs_idx.insert(*b, i);
}
let mut idoms: HashMap<BlockId, Option<BlockId>> = HashMap::new();
for (i, b) in dfs_order.iter().enumerate() {
if i == 0 {
idoms.insert(*b, None);
} else {
idoms.insert(*b, None); }
}
let mut changed = true;
while changed {
changed = false;
for b in dfs_order.iter().skip(1) {
let preds = pred_map.get(b);
if preds.is_none() || preds.unwrap().is_empty() {
continue;
}
let preds = preds.unwrap();
let mut new_idom: Option<BlockId> = None;
for p in preds {
if let Some(idom) = idoms.get(p) {
if idom.is_some() || *p == entry {
new_idom = Some(*p);
break;
}
}
}
for p in preds.iter().skip(1) {
if let Some(current) = new_idom {
if idoms.get(p).map_or(false, |v| v.is_some()) || *p == entry {
new_idom = Some(self.intersect(&dfs_idx, &idoms, current, *p, entry));
}
}
}
if idoms.get(b) != Some(&new_idom) {
idoms.insert(*b, new_idom);
changed = true;
}
}
}
let mut children: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
for (b, idom_opt) in &idoms {
if let Some(idom) = idom_opt {
children.entry(*idom).or_default().push(*b);
}
}
let mut dfs_in: HashMap<BlockId, usize> = HashMap::new();
let mut dfs_out: HashMap<BlockId, usize> = HashMap::new();
let mut counter: usize = 0;
self.assign_dfs_numbers(entry, &children, &mut dfs_in, &mut dfs_out, &mut counter);
DominatorTree {
idom: idoms
.into_iter()
.filter_map(|(k, v)| v.map(|v| (k as usize, v as usize)))
.collect(),
children: children
.into_iter()
.map(|(k, v)| (k as usize, v.into_iter().map(|x| x as usize).collect()))
.collect(),
dom_level: HashMap::new(),
dfs_in: dfs_in
.into_iter()
.map(|(k, v)| (k as usize, v as u32))
.collect(),
dfs_out: dfs_out
.into_iter()
.map(|(k, v)| (k as usize, v as u32))
.collect(),
dfs_order: Vec::new(),
}
}
fn dfs_block(
&self,
current: BlockId,
blocks: &HashMap<BlockId, Vec<ValueId>>,
pred_map: &HashMap<BlockId, Vec<BlockId>>,
order: &mut Vec<BlockId>,
visited: &mut HashSet<BlockId>,
) {
if visited.contains(¤t) || !blocks.contains_key(¤t) {
return;
}
visited.insert(current);
order.push(current);
for (b, preds) in pred_map {
if preds.contains(¤t) && !visited.contains(b) {
self.dfs_block(*b, blocks, pred_map, order, visited);
}
}
}
fn intersect(
&self,
dfs_idx: &HashMap<BlockId, usize>,
idoms: &HashMap<BlockId, Option<BlockId>>,
mut b1: BlockId,
mut b2: BlockId,
_entry: BlockId,
) -> BlockId {
while b1 != b2 {
let d1 = dfs_idx.get(&b1).copied().unwrap_or(usize::MAX);
let d2 = dfs_idx.get(&b2).copied().unwrap_or(usize::MAX);
if d1 > d2 {
if let Some(Some(idom)) = idoms.get(&b1) {
b1 = *idom;
} else {
return b1;
}
} else if let Some(Some(idom)) = idoms.get(&b2) {
b2 = *idom;
} else {
return b2;
}
}
b1
}
fn assign_dfs_numbers(
&self,
node: BlockId,
children: &HashMap<BlockId, Vec<BlockId>>,
dfs_in: &mut HashMap<BlockId, usize>,
dfs_out: &mut HashMap<BlockId, usize>,
counter: &mut usize,
) {
*counter += 1;
dfs_in.insert(node, *counter);
if let Some(kids) = children.get(&node) {
for child in kids {
self.assign_dfs_numbers(*child, children, dfs_in, dfs_out, counter);
}
}
*counter += 1;
dfs_out.insert(node, *counter);
}
fn find_back_edges(
&self,
blocks: &HashMap<BlockId, Vec<ValueId>>,
succ_map: &HashMap<BlockId, Vec<BlockId>>,
dom_tree: &DominatorTree,
) -> Vec<(BlockId, BlockId)> {
let mut backedges = Vec::new();
for (src, succs) in succ_map {
for tgt in succs {
if self.dominates(*tgt, *src, dom_tree) {
backedges.push((*src, *tgt));
}
}
}
backedges
}
fn dominates(&self, a: BlockId, b: BlockId, dom_tree: &DominatorTree) -> bool {
let a = a as usize;
let b = b as usize;
let dfs_in = &dom_tree.dfs_in;
let dfs_out = &dom_tree.dfs_out;
match (
dfs_in.get(&a),
dfs_out.get(&a),
dfs_in.get(&b),
dfs_out.get(&b),
) {
(Some(ai), Some(ao), Some(bi), Some(bo)) => ai <= bi && bo <= ao,
_ => false,
}
}
fn discover_loop_body(
&self,
header: BlockId,
backedge_src: BlockId,
blocks: &HashMap<BlockId, Vec<ValueId>>,
succ_map: &HashMap<BlockId, Vec<BlockId>>,
) -> Vec<BlockId> {
let mut body: Vec<BlockId> = vec![header];
let mut worklist: VecDeque<BlockId> = VecDeque::new();
worklist.push_back(backedge_src);
while let Some(block) = worklist.pop_front() {
if block == header || body.contains(&block) {
continue;
}
if !blocks.contains_key(&block) {
continue;
}
body.push(block);
for (pred, succs) in succ_map {
if succs.contains(&block) && !body.contains(pred) {
worklist.push_back(*pred);
}
}
}
body
}
fn collect_backedges_for_header(
&self,
header: BlockId,
all_backedges: &[(BlockId, BlockId)],
) -> Vec<(BlockId, BlockId)> {
all_backedges
.iter()
.filter(|(_, tgt)| *tgt == header)
.copied()
.collect()
}
fn compute_exit_info(
&self,
header: BlockId,
loop_blocks: &[BlockId],
succ_map: &HashMap<BlockId, Vec<BlockId>>,
) -> (Vec<BlockId>, Vec<BlockId>, Vec<(BlockId, BlockId)>) {
let mut exiting: Vec<BlockId> = Vec::new();
let mut exit_blocks: Vec<BlockId> = Vec::new();
let mut exit_edges: Vec<(BlockId, BlockId)> = Vec::new();
for &block in loop_blocks {
if let Some(succs) = succ_map.get(&block) {
for succ in succs {
if !loop_blocks.contains(succ) {
if !exiting.contains(&block) {
exiting.push(block);
}
if !exit_blocks.contains(succ) {
exit_blocks.push(*succ);
}
let edge = (block, *succ);
if !exit_edges.contains(&edge) {
exit_edges.push(edge);
}
}
}
}
}
(exiting, exit_blocks, exit_edges)
}
fn is_loop_reducible(
&self,
header: BlockId,
loop_blocks: &[BlockId],
pred_map: &HashMap<BlockId, Vec<BlockId>>,
) -> bool {
for &block in loop_blocks {
if let Some(preds) = pred_map.get(&block) {
for pred in preds {
if !loop_blocks.contains(pred) && *pred != header {
return false; }
}
}
}
true
}
fn find_preheader(
&self,
header: BlockId,
loop_blocks: &[BlockId],
pred_map: &HashMap<BlockId, Vec<BlockId>>,
) -> Option<BlockId> {
let preds = pred_map.get(&header)?;
let outside_preds: Vec<&BlockId> =
preds.iter().filter(|p| !loop_blocks.contains(p)).collect();
if outside_preds.len() == 1 {
Some(*outside_preds[0])
} else {
None }
}
fn estimate_trip_count(&self, _header: BlockId, loop_blocks: &[BlockId]) -> TripCountEstimate {
if loop_blocks.is_empty() {
return TripCountEstimate::Exact(0);
}
TripCountEstimate::Unknown
}
fn estimate_body_size(
&self,
loop_blocks: &[BlockId],
blocks: &HashMap<BlockId, Vec<ValueId>>,
) -> usize {
let mut total: usize = 0;
for b in loop_blocks {
if let Some(instrs) = blocks.get(b) {
total += instrs.len();
}
}
total
}
fn estimate_uop_count(
&self,
loop_blocks: &[BlockId],
blocks: &HashMap<BlockId, Vec<ValueId>>,
) -> usize {
let inst_count = self.estimate_body_size(loop_blocks, blocks);
(inst_count as f64 * 1.2) as usize
}
fn loop_contains_calls(
&self,
_loop_blocks: &[BlockId],
_blocks: &HashMap<BlockId, Vec<ValueId>>,
) -> bool {
false
}
fn loop_contains_memory_ops(
&self,
_loop_blocks: &[BlockId],
_blocks: &HashMap<BlockId, Vec<ValueId>>,
) -> bool {
true
}
fn compute_loop_nesting(&mut self) {
if self.loops.len() <= 1 {
for l in &mut self.loops {
l.depth = 0;
}
return;
}
let mut indices: Vec<usize> = (0..self.loops.len()).collect();
indices.sort_by_key(|&i| self.loops[i].blocks.len());
for i in 0..self.loops.len() {
let blocks_i: HashSet<BlockId> = self.loops[i].blocks.iter().copied().collect();
for j in 0..self.loops.len() {
if i == j {
continue;
}
let blocks_j: HashSet<BlockId> = self.loops[j].blocks.iter().copied().collect();
if blocks_j.is_subset(&blocks_i) && blocks_j.len() < blocks_i.len() {
let child_id = self.loops[j].id;
if !self.loops[i].children.contains(&child_id) {
self.loops[i].children.push(child_id);
}
let parent_id = self.loops[i].id;
self.loops[j].parent = Some(parent_id);
}
}
}
for i in 0..self.loops.len() {
let depth = self.compute_loop_depth(self.loops[i].id);
self.loops[i].depth = depth;
}
}
fn compute_loop_depth(&self, loop_id: u64) -> LoopDepth {
let mut depth: LoopDepth = 0;
let mut current = loop_id;
loop {
let parent = self
.loops
.iter()
.find(|l| l.id == current)
.and_then(|l| l.parent);
match parent {
Some(p) => {
depth += 1;
current = p;
}
None => break,
}
}
depth
}
pub fn get_loop(&self, loop_id: u64) -> Option<&X86NaturalLoop> {
self.loops.iter().find(|l| l.id == loop_id)
}
pub fn get_loop_mut(&mut self, loop_id: u64) -> Option<&mut X86NaturalLoop> {
self.loops.iter_mut().find(|l| l.id == loop_id)
}
pub fn loops_by_depth(&self) -> Vec<&X86NaturalLoop> {
let mut loops: Vec<&X86NaturalLoop> = self.loops.iter().collect();
loops.sort_by_key(|l| std::cmp::Reverse(l.depth));
loops
}
pub fn is_loop_header(&self, block: BlockId) -> bool {
self.block_to_loop.contains_key(&block)
&& self
.loops
.iter()
.any(|l| l.header == block && self.block_to_loop.get(&block) == Some(&l.id))
}
pub fn get_innermost_loop_for_block(&self, block: BlockId) -> Option<&X86NaturalLoop> {
let loop_id = self.block_to_loop.get(&block)?;
let mut current = *loop_id;
loop {
let children: Vec<u64> = self
.loops
.iter()
.find(|l| l.id == current)
.map(|l| l.children.clone())
.unwrap_or_default();
let child_containing = children.into_iter().find(|&cid| {
self.loops
.iter()
.find(|l| l.id == cid)
.map_or(false, |l| l.blocks.contains(&block))
});
match child_containing {
Some(cid) => current = cid,
None => break,
}
}
self.loops.iter().find(|l| l.id == current)
}
pub fn run_loop_simplify(&mut self) -> usize {
let mut simplified = 0usize;
let loop_count = self.loops.len();
let mut to_simplify: Vec<usize> = Vec::new();
for i in 0..loop_count {
if !self.loops[i].is_canonical {
to_simplify.push(i);
}
}
for idx in to_simplify {
if self.simplify_loop(idx) {
simplified += 1;
}
}
self.stats.simplified += simplified;
simplified
}
fn simplify_loop(&mut self, loop_idx: usize) -> bool {
if loop_idx >= self.loops.len() {
return false;
}
let loop_info = &self.loops[loop_idx];
let header = loop_info.header;
let latches = loop_info.latches.clone();
if latches.len() == 1 && loop_info.preheader.is_some() {
if let Some(l) = self.loops.get_mut(loop_idx) {
l.is_canonical = true;
l.canonical_latch = Some(latches[0]);
}
return false;
}
if let Some(l) = self.loops.get_mut(loop_idx) {
l.is_canonical = true;
l.canonical_latch = latches.first().copied();
}
true
}
pub fn run_idiom_recognition(&mut self) -> usize {
let mut recognized = 0usize;
for i in 0..self.loops.len() {
let loop_info = &self.loops[i];
if self.try_recognize_memset(loop_info) {
recognized += 1;
continue;
}
if self.try_recognize_memcpy(loop_info) {
recognized += 1;
continue;
}
if self.try_recognize_popcount(loop_info) {
recognized += 1;
continue;
}
if self.try_recognize_strlen(loop_info) {
recognized += 1;
continue;
}
if self.try_recognize_memcmp(loop_info) {
recognized += 1;
continue;
}
}
self.stats.idioms_recognized += recognized;
recognized
}
fn try_recognize_memset(&self, loop_info: &X86NaturalLoop) -> bool {
if loop_info.blocks.len() != 1 && loop_info.blocks.len() != 2 {
return false;
}
if !loop_info.contains_memory_ops {
return false;
}
loop_info.blocks.len() == 1 && loop_info.induction_vars.len() >= 1
}
fn try_recognize_memcpy(&self, loop_info: &X86NaturalLoop) -> bool {
if loop_info.blocks.len() > 2 {
return false;
}
if !loop_info.contains_memory_ops {
return false;
}
loop_info.induction_vars.len() >= 2
}
fn try_recognize_popcount(&self, loop_info: &X86NaturalLoop) -> bool {
if loop_info.blocks.len() == 1 && !loop_info.contains_memory_ops {
if let TripCountEstimate::Exact(n) = &loop_info.trip_count {
if *n <= 64 {
return true;
}
}
}
false
}
fn try_recognize_strlen(&self, _loop_info: &X86NaturalLoop) -> bool {
false }
fn try_recognize_memcmp(&self, _loop_info: &X86NaturalLoop) -> bool {
false }
pub fn run_loop_deletion(&mut self) -> usize {
let mut deleted = 0usize;
let sorted: Vec<usize> = {
let mut indices: Vec<usize> = (0..self.loops.len()).collect();
indices.sort_by_key(|&i| std::cmp::Reverse(self.loops[i].depth));
indices
};
for idx in sorted {
if self.is_loop_dead(idx) {
deleted += 1;
}
}
self.stats.deleted += deleted;
deleted
}
fn is_loop_dead(&self, loop_idx: usize) -> bool {
let loop_info = &self.loops[loop_idx];
if loop_info.contains_calls {
return false;
}
if loop_info.contains_memory_ops {
return false;
}
if !loop_info.trip_count.has_bound() {
return false;
}
if float_loop_might_be_infinite(loop_info) {
return false;
}
if !loop_info.exit_edges.is_empty() && loop_info.exit_blocks.len() > 1 {
return false;
}
true
}
pub fn compute_scev_for_loops(&mut self, func: &ValueRef) {
let mut scev = ScalarEvolution::new(func);
for loop_info in &self.loops {
if loop_info.is_canonical {
let _ = (loop_info.header, &loop_info.blocks);
}
}
self.scev = Some(scev);
}
pub fn get_scev_for_value(&self, value: ValueId, _loop_id: u64) -> Option<SCEV> {
let _ = (value, _loop_id);
None
}
pub fn analyze_induction_vars(&mut self) -> usize {
let mut total_ivs = 0usize;
let loop_count = self.loops.len();
for i in 0..loop_count {
let loop_info = &self.loops[i];
let ivs = self.find_induction_vars(loop_info);
total_ivs += ivs.len();
if let Some(l) = self.loops.get_mut(i) {
l.induction_vars = ivs;
}
}
total_ivs
}
fn find_induction_vars(&self, loop_info: &X86NaturalLoop) -> Vec<InductionVariable> {
let mut ivs = Vec::new();
let mut next_value_id: u64 = 1000;
if loop_info.blocks.len() >= 1 {
let basic_iv = InductionVariable::new_basic(next_value_id, 0, 1, loop_info.id, 32);
next_value_id += 1;
ivs.push(basic_iv);
if loop_info.contains_memory_ops {
let derived_iv = InductionVariable::new_derived(
next_value_id,
next_value_id - 1,
8, 0,
loop_info.id,
64,
);
ivs.push(derived_iv);
}
}
for iv in &mut ivs {
if let Some(scev_expr) = self.get_scev_for_value(iv.value, loop_info.id) {
iv.scev = Some(scev_expr);
}
}
ivs
}
pub fn run_loop_rotation(&mut self) -> usize {
let mut rotated = 0usize;
for i in 0..self.loops.len() {
if self.should_rotate_loop(i) {
if self.rotate_loop(i) {
rotated += 1;
}
}
}
self.stats.loops_rotated += rotated;
rotated
}
fn should_rotate_loop(&self, loop_idx: usize) -> bool {
let loop_info = &self.loops[loop_idx];
if !loop_info.is_reducible {
return false;
}
if loop_info.preheader.is_none() {
return false;
}
if loop_info.blocks.len() < 2 {
return false;
}
if loop_info.latches.len() == 1 && loop_info.is_canonical {
return false;
}
true
}
fn rotate_loop(&mut self, loop_idx: usize) -> bool {
true
}
pub fn run_loop_interchange(&mut self) {
let mut interchanged = 0usize;
for outer in 0..self.loops.len() {
for &inner_id in &self.loops[outer].children.clone() {
let inner_idx = inner_id as usize;
if self.can_interchange(outer, inner_idx) {
if self.is_profitable_to_interchange(outer, inner_idx) {
let _ = self.interchange_loops(outer, inner_idx);
interchanged += 1;
}
}
}
}
self.stats.interchanged += interchanged;
}
fn can_interchange(&self, outer_idx: usize, inner_idx: usize) -> bool {
if outer_idx >= self.loops.len() || inner_idx >= self.loops.len() {
return false;
}
let outer = &self.loops[outer_idx];
let inner = &self.loops[inner_idx];
if !self.is_perfectly_nested(outer, inner) {
return false;
}
if !outer.is_reducible || !inner.is_reducible {
return false;
}
if !self.have_permutable_dependences(outer, inner) {
return false;
}
if outer.depth > self.cost_config.max_loop_depth_interchange {
return false;
}
true
}
fn is_perfectly_nested(&self, outer: &X86NaturalLoop, inner: &X86NaturalLoop) -> bool {
inner.blocks.iter().all(|b| outer.blocks.contains(b))
}
fn have_permutable_dependences(
&self,
_outer: &X86NaturalLoop,
_inner: &X86NaturalLoop,
) -> bool {
true
}
fn is_profitable_to_interchange(&self, _outer_idx: usize, inner_idx: usize) -> bool {
let inner = &self.loops[inner_idx];
inner.contains_memory_ops && inner.induction_vars.len() >= 2
}
fn interchange_loops(&mut self, _outer_idx: usize, _inner_idx: usize) -> usize {
1
}
pub fn run_loop_unswitching(&mut self) -> usize {
let mut unswitched = 0usize;
for i in 0..self.loops.len() {
let loop_info = &self.loops[i];
let invariants = loop_info.invariants.clone();
if invariants.is_empty() {
continue;
}
if self.has_invariant_branches(loop_info) {
if self.is_profitable_to_unswitch(loop_info) {
if self.unswitch_loop(i) {
unswitched += 1;
}
}
}
}
self.stats.unswitched += unswitched;
unswitched
}
fn has_invariant_branches(&self, loop_info: &X86NaturalLoop) -> bool {
!loop_info.invariants.is_empty() && loop_info.blocks.len() > 1
}
fn is_profitable_to_unswitch(&self, loop_info: &X86NaturalLoop) -> bool {
match &loop_info.trip_count {
TripCountEstimate::Exact(n) if *n >= 8 => true,
TripCountEstimate::Max(n) if *n >= 16 => true,
_ => false,
}
}
fn unswitch_loop(&mut self, _loop_idx: usize) -> bool {
true
}
pub fn run_loop_distribution(&mut self) -> usize {
let mut distributed = 0usize;
for i in 0..self.loops.len() {
let loop_info = &self.loops[i];
if loop_info.body_size < 5 {
continue;
}
if self.can_distribute(loop_info) && self.is_profitable_to_distribute(loop_info) {
if self.distribute_loop(i) {
distributed += 1;
}
}
}
self.stats.distributed += distributed;
distributed
}
fn can_distribute(&self, loop_info: &X86NaturalLoop) -> bool {
loop_info.body_size >= 4 && !loop_info.contains_calls
}
fn is_profitable_to_distribute(&self, loop_info: &X86NaturalLoop) -> bool {
loop_info.contains_memory_ops && loop_info.body_size >= 8
}
fn distribute_loop(&mut self, _loop_idx: usize) -> bool {
true
}
pub fn run_loop_fusion(&mut self) -> usize {
let mut fused = 0usize;
let loop_count = self.loops.len();
let mut fused_set: HashSet<u64> = HashSet::new();
for i in 0..loop_count {
if fused_set.contains(&self.loops[i].id) {
continue;
}
for j in (i + 1)..loop_count {
if fused_set.contains(&self.loops[j].id) {
continue;
}
if self.can_fuse(i, j) && self.is_profitable_to_fuse(i, j) {
if self.fuse_loops(i, j) {
fused += 1;
fused_set.insert(self.loops[i].id);
fused_set.insert(self.loops[j].id);
}
}
}
}
self.stats.fused += fused;
fused
}
fn can_fuse(&self, i: usize, j: usize) -> bool {
let a = &self.loops[i];
let b = &self.loops[j];
if a.depth != b.depth {
return false;
}
if !a.is_reducible || !b.is_reducible {
return false;
}
match (&a.trip_count, &b.trip_count) {
(TripCountEstimate::Exact(ta), TripCountEstimate::Exact(tb)) if ta == tb => {}
(TripCountEstimate::Unknown, _) | (_, TripCountEstimate::Unknown) => {
return false;
}
_ => return false,
}
true
}
fn is_profitable_to_fuse(&self, i: usize, j: usize) -> bool {
let a = &self.loops[i];
let b = &self.loops[j];
let combined_uops = a.uop_count + b.uop_count;
if combined_uops > self.cost_config.uop_cache_capacity {
return false; }
a.contains_memory_ops || b.contains_memory_ops
}
fn fuse_loops(&mut self, _i: usize, _j: usize) -> bool {
true
}
pub fn run_strength_reduction(&mut self) -> usize {
let mut reduced = 0usize;
for i in 0..self.loops.len() {
let loop_info = &self.loops[i];
if loop_info.body_size < self.cost_config.min_body_size_strength_reduce {
continue;
}
if !loop_info.induction_vars.is_empty() {
if self.apply_strength_reduction(i) {
reduced += 1;
}
}
}
self.stats.strength_reduced += reduced;
reduced
}
fn apply_strength_reduction(&mut self, loop_idx: usize) -> bool {
let loop_info = &self.loops[loop_idx];
for iv in &loop_info.induction_vars {
if iv.step != 1 && iv.step != 0 {
continue;
}
}
self.stats.ivs_optimized += loop_info.induction_vars.len();
true
}
pub fn run_loop_unrolling(&mut self) -> usize {
let mut fully_unrolled = 0usize;
let mut partially_unrolled = 0usize;
let sorted = self.loops_by_depth();
let loop_ids: Vec<u64> = sorted.iter().map(|l| l.id).collect();
for &loop_id in &loop_ids {
let idx = self.loops.iter().position(|l| l.id == loop_id);
if idx.is_none() {
continue;
}
let idx = idx.unwrap();
if self.try_full_unroll(idx) {
fully_unrolled += 1;
continue;
}
if self.try_partial_unroll(idx) {
partially_unrolled += 1;
}
}
self.stats.fully_unrolled += fully_unrolled;
self.stats.partially_unrolled += partially_unrolled;
fully_unrolled + partially_unrolled
}
fn try_full_unroll(&mut self, loop_idx: usize) -> bool {
let loop_info = &self.loops[loop_idx];
if !self.cost_config.should_full_unroll(
&loop_info.trip_count,
loop_info.body_size,
loop_info.contains_calls,
) {
return false;
}
let trip = loop_info.trip_count.as_exact().unwrap();
let unrolled_size = loop_info.body_size * trip as usize;
if unrolled_size > self.cost_config.max_full_unroll_insts {
return false;
}
if !loop_info.is_reducible {
return false;
}
true
}
fn try_partial_unroll(&mut self, loop_idx: usize) -> bool {
let loop_info = &self.loops[loop_idx];
if !self.cost_config.should_partial_unroll(
&loop_info.trip_count,
loop_info.uop_count,
loop_info.contains_calls,
) {
return false;
}
let factor = self
.cost_config
.compute_unroll_factor(&loop_info.trip_count, loop_info.uop_count);
if factor <= 1 {
return false;
}
if self.cost_config.lsd_available {
let unrolled_uops = loop_info.uop_count * factor as usize;
if unrolled_uops > self.cost_config.lsd_capacity {
let max_factor =
(self.cost_config.lsd_capacity / loop_info.uop_count.max(1)) as u32;
if max_factor <= 1 {
return false;
}
}
}
true
}
pub fn run_unroll_and_jam(&mut self) -> usize {
let mut jammed = 0usize;
for outer in 0..self.loops.len() {
let children = self.loops[outer].children.clone();
for &inner_id in &children {
let inner_idx = inner_id as usize;
if self.can_unroll_and_jam(outer, inner_idx) {
if self.is_profitable_unroll_and_jam(outer, inner_idx) {
let _ = self.unroll_and_jam(outer, inner_idx);
jammed += 1;
}
}
}
}
self.stats.unroll_and_jammed += jammed;
jammed
}
fn can_unroll_and_jam(&self, outer_idx: usize, inner_idx: usize) -> bool {
let outer = &self.loops[outer_idx];
let inner = &self.loops[inner_idx];
if !self.is_perfectly_nested(outer, inner) {
return false;
}
if !outer.is_reducible || !inner.is_reducible {
return false;
}
match &outer.trip_count {
TripCountEstimate::Exact(n) if *n >= 2 => {}
_ => return false,
}
true
}
fn is_profitable_unroll_and_jam(&self, outer_idx: usize, inner_idx: usize) -> bool {
let outer = &self.loops[outer_idx];
let inner = &self.loops[inner_idx];
let combined_uops =
(outer.uop_count + inner.uop_count) * self.cost_config.max_unroll_jam_factor as usize;
combined_uops <= self.cost_config.uop_cache_capacity
}
fn unroll_and_jam(&mut self, _outer_idx: usize, _inner_idx: usize) -> usize {
1
}
pub fn run_loop_versioning(&mut self) -> usize {
let mut versioned = 0usize;
for i in 0..self.loops.len() {
let has_mem_ops = self.loops[i].contains_memory_ops;
let body_size = self.loops[i].body_size;
let can_alias = has_mem_ops && self.loops[i].induction_vars.len() >= 2;
let can_align = has_mem_ops && !self.loops[i].induction_vars.is_empty();
if has_mem_ops && body_size >= 3 {
if can_alias {
if self.create_alias_version(i) {
versioned += 1;
}
}
if can_align {
if self.create_alignment_version(i) {
versioned += 1;
}
}
}
}
self.stats.versioned += versioned;
versioned
}
fn can_version_for_alias(&self, loop_info: &X86NaturalLoop) -> bool {
loop_info.contains_memory_ops && loop_info.induction_vars.len() >= 2
}
fn can_version_for_alignment(&self, loop_info: &X86NaturalLoop) -> bool {
loop_info.contains_memory_ops && !loop_info.induction_vars.is_empty()
}
fn create_alias_version(&mut self, _loop_idx: usize) -> bool {
true
}
fn create_alignment_version(&mut self, _loop_idx: usize) -> bool {
true
}
pub fn run_loop_predication(&mut self) -> usize {
let mut predicated = 0usize;
for i in 0..self.loops.len() {
let loop_info = &self.loops[i];
if loop_info.body_size > self.cost_config.max_predication_body_size {
continue;
}
if !loop_info.is_reducible {
continue;
}
if self.has_divergent_branches(loop_info) {
if self.predicate_loop(i) {
predicated += 1;
}
}
}
self.stats.predicated += predicated;
predicated
}
fn has_divergent_branches(&self, loop_info: &X86NaturalLoop) -> bool {
loop_info.contains_memory_ops && loop_info.blocks.len() >= 2
}
fn predicate_loop(&mut self, _loop_idx: usize) -> bool {
true
}
pub fn run_loop_rerolling(&mut self) -> usize {
let mut rerolled = 0usize;
for i in 0..self.loops.len() {
let loop_info = &self.loops[i];
if loop_info.body_size > 50 {
if self.is_candidate_for_reroll(loop_info) {
if self.reroll_loop(i) {
rerolled += 1;
}
}
}
}
self.stats.rerolled += rerolled;
rerolled
}
fn is_candidate_for_reroll(&self, loop_info: &X86NaturalLoop) -> bool {
match &loop_info.trip_count {
TripCountEstimate::Exact(n) if *n <= 4 => loop_info.body_size > 20,
_ => false,
}
}
fn reroll_loop(&mut self, _loop_idx: usize) -> bool {
true
}
pub fn run_x86_tuning(&mut self) -> usize {
let mut tunings = 0usize;
for i in 0..self.loops.len() {
tunings += self.align_loop_header(i);
tunings += self.insert_nop_padding(i);
tunings += self.optimize_loop_branch_for_btb(i);
tunings += self.lsd_aware_sizing(i);
tunings += self.insert_prefetch_instructions(i);
}
tunings
}
pub fn align_loop_header(&mut self, loop_idx: usize) -> usize {
if !self.cost_config.align_loop_headers {
return 0;
}
let loop_info = &self.loops[loop_idx];
let alignment = self.cost_config.loop_alignment;
if loop_info.uop_count < 4 {
return 0;
}
self.stats.headers_aligned += 1;
1
}
pub fn insert_nop_padding(&mut self, loop_idx: usize) -> usize {
let loop_info = &self.loops[loop_idx];
let alignment = self.microarch.preferred_loop_alignment();
let current_offset: u32 = 0; let misalignment = current_offset % alignment;
if misalignment == 0 {
return 0;
}
let padding = alignment - misalignment;
self.stats.nop_bytes_padded += padding as usize;
1
}
pub fn optimize_loop_branch_for_btb(&mut self, loop_idx: usize) -> usize {
let loop_info = &self.loops[loop_idx];
if loop_info.latches.len() == 1 {
return 1;
}
0
}
pub fn lsd_aware_sizing(&mut self, loop_idx: usize) -> usize {
if !self.cost_config.lsd_available {
return 0;
}
let loop_info = &self.loops[loop_idx];
if loop_info.blocks.len() > 1 {
return 0;
}
if loop_info.uop_count <= self.cost_config.lsd_capacity {
return 1;
}
0
}
pub fn insert_prefetch_instructions(&mut self, loop_idx: usize) -> usize {
if !self.cost_config.insert_prefetches {
return 0;
}
let loop_info = &self.loops[loop_idx];
if !loop_info.contains_memory_ops {
return 0;
}
if loop_info.body_size < 5 {
return 0;
}
match &loop_info.trip_count {
TripCountEstimate::Exact(n) if *n >= 16 => {}
TripCountEstimate::Max(n) if *n >= 32 => {}
TripCountEstimate::Unknown => {
}
_ => return 0,
}
let _prefetch_distance = 8usize;
self.stats.prefetches_inserted += 1;
1
}
pub fn choose_prefetch_type(&self, _stride: i64, _is_write: bool) -> PrefetchType {
PrefetchType::T0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrefetchType {
T0,
T1,
T2,
NTA,
}
impl PrefetchType {
pub fn mnemonic_suffix(&self) -> &'static str {
match self {
PrefetchType::T0 => "t0",
PrefetchType::T1 => "t1",
PrefetchType::T2 => "t2",
PrefetchType::NTA => "nta",
}
}
}
pub struct LoopInvariantDetector {
pub loop_invariants: HashMap<u64, HashSet<ValueId>>,
pub conservative_memory: bool,
}
impl LoopInvariantDetector {
pub fn new() -> Self {
Self {
loop_invariants: HashMap::new(),
conservative_memory: true,
}
}
pub fn detect_invariants(
&mut self,
optimizer: &mut X86LoopOptimizer,
blocks: &HashMap<BlockId, Vec<ValueId>>,
) {
let loop_ids: Vec<u64> = optimizer.loops.iter().map(|l| l.id).collect();
for loop_id in loop_ids {
let invariants = {
let loop_info = optimizer.get_loop(loop_id).unwrap();
self.find_invariants_in_loop(loop_info, blocks)
};
self.loop_invariants.insert(loop_id, invariants.clone());
if let Some(l) = optimizer.get_loop_mut(loop_id) {
l.invariants = invariants.into_iter().collect();
}
}
}
fn find_invariants_in_loop(
&self,
_loop_info: &X86NaturalLoop,
_blocks: &HashMap<BlockId, Vec<ValueId>>,
) -> HashSet<ValueId> {
HashSet::new()
}
}
pub struct X86SCEV {
pub scev: Option<ScalarEvolution>,
pub microarch: X86MicroArch,
pub scev_cache: HashMap<(ValueId, u64), Option<SCEV>>,
}
impl X86SCEV {
pub fn new(microarch: X86MicroArch) -> Self {
Self {
scev: None,
microarch,
scev_cache: HashMap::new(),
}
}
pub fn get_scev(&mut self, _value: ValueId, _loop_id: u64) -> Option<SCEV> {
None }
pub fn is_add_rec(&self, scev: &SCEV) -> bool {
matches!(scev, SCEV::AddRec { .. })
}
pub fn decompose_add_rec<'a>(&self, scev: &'a SCEV) -> Option<(&'a SCEV, &'a SCEV, u64)> {
match scev {
SCEV::AddRec {
base,
step,
loop_header,
..
} => Some((base, step, *loop_header)),
_ => None,
}
}
pub fn trip_count_from_exit(
&self,
add_rec: &SCEV,
bound: &SCEV,
predicate: ICmpPred,
) -> Option<TripCountEstimate> {
let (_base, step, _loop_header) = self.decompose_add_rec(add_rec)?;
match (step, bound) {
(SCEV::Constant(step_val), SCEV::Constant(bound_val)) => {
if *step_val == 0 {
return None;
}
let base_val = match add_rec {
SCEV::AddRec { base, .. } => match base.as_ref() {
SCEV::Constant(v) => *v,
_ => return None,
},
_ => return None,
};
let diff = bound_val - base_val;
let count = if *step_val > 0 {
(diff + step_val - 1) / step_val
} else {
(base_val - bound_val + (-step_val) - 1) / (-step_val)
};
if count >= 0 {
match predicate {
ICmpPred::Slt | ICmpPred::Ult | ICmpPred::Ne => {
Some(TripCountEstimate::Exact(count.max(0) as u64))
}
ICmpPred::Sle | ICmpPred::Ule => {
Some(TripCountEstimate::Exact((count.max(0) + 1) as u64))
}
ICmpPred::Eq => {
if diff == 0 {
Some(TripCountEstimate::Exact(0))
} else {
Some(TripCountEstimate::Unknown)
}
}
_ => Some(TripCountEstimate::Unknown),
}
} else {
Some(TripCountEstimate::Exact(0))
}
}
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopAlignmentPolicy {
None,
Align16,
Align32,
Align64,
Auto,
}
impl LoopAlignmentPolicy {
pub fn for_microarch(microarch: X86MicroArch) -> Self {
match microarch {
X86MicroArch::Core2 | X86MicroArch::Nehalem => LoopAlignmentPolicy::Align16,
X86MicroArch::SandyBridge | X86MicroArch::Haswell | X86MicroArch::Skylake => {
LoopAlignmentPolicy::Align16
}
X86MicroArch::IceLake | X86MicroArch::AlderLakeP | X86MicroArch::AlderLakeE => {
LoopAlignmentPolicy::Align32
}
X86MicroArch::GraniteRapids => LoopAlignmentPolicy::Align64,
X86MicroArch::Zen1 | X86MicroArch::Zen2 => LoopAlignmentPolicy::Align16,
X86MicroArch::Zen3 | X86MicroArch::Zen4 => LoopAlignmentPolicy::Align32,
X86MicroArch::Zen5 => LoopAlignmentPolicy::Align64,
_ => LoopAlignmentPolicy::Align16,
}
}
pub fn alignment_bytes(&self) -> u32 {
match self {
LoopAlignmentPolicy::None => 1,
LoopAlignmentPolicy::Align16 => 16,
LoopAlignmentPolicy::Align32 => 32,
LoopAlignmentPolicy::Align64 => 64,
LoopAlignmentPolicy::Auto => 16,
}
}
}
pub struct X86NopGenerator {
pub microarch: X86MicroArch,
pub total_bytes_padded: usize,
}
impl X86NopGenerator {
pub fn new(microarch: X86MicroArch) -> Self {
Self {
microarch,
total_bytes_padded: 0,
}
}
pub fn generate_nops(&mut self, byte_count: u32) -> Vec<u8> {
let mut result = Vec::new();
let mut remaining = byte_count;
while remaining >= 9 {
if self.microarch as u32 >= X86MicroArch::SandyBridge as u32 {
result.extend_from_slice(&[0x66, 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00]);
} else {
result.extend(std::iter::repeat(0x90u8).take(9));
}
remaining -= 9;
}
while remaining >= 8 {
result.extend_from_slice(&[0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00]);
remaining -= 8;
}
while remaining >= 7 {
result.extend_from_slice(&[0x0F, 0x1F, 0x80, 0x00, 0x00, 0x00, 0x00]);
remaining -= 7;
}
while remaining >= 6 {
result.extend_from_slice(&[0x66, 0x0F, 0x1F, 0x44, 0x00, 0x00]);
remaining -= 6;
}
while remaining >= 5 {
result.extend_from_slice(&[0x0F, 0x1F, 0x44, 0x00, 0x00]);
remaining -= 5;
}
while remaining >= 4 {
result.extend_from_slice(&[0x0F, 0x1F, 0x40, 0x00]);
remaining -= 4;
}
while remaining >= 3 {
result.extend_from_slice(&[0x0F, 0x1F, 0x00]);
remaining -= 3;
}
while remaining >= 2 {
result.extend_from_slice(&[0x66, 0x90]);
remaining -= 2;
}
while remaining >= 1 {
result.push(0x90);
remaining -= 1;
}
self.total_bytes_padded += result.len();
result
}
pub fn compute_padding(current_offset: u32, alignment: u32) -> u32 {
let remainder = current_offset % alignment;
if remainder == 0 {
0
} else {
alignment - remainder
}
}
}
pub struct X86BTBOptimizer {
pub btb_size: usize,
pub btb_associativity: usize,
pub has_indirect_branch_prediction: bool,
}
impl X86BTBOptimizer {
pub fn new(microarch: X86MicroArch) -> Self {
Self {
btb_size: microarch.btb_entries(),
btb_associativity: 4,
has_indirect_branch_prediction: matches!(
microarch,
X86MicroArch::Haswell
| X86MicroArch::Skylake
| X86MicroArch::IceLake
| X86MicroArch::AlderLakeP
| X86MicroArch::Zen3
| X86MicroArch::Zen4
| X86MicroArch::Zen5
),
}
}
pub fn has_btb_alias(&self, branch_addr: u64, target_addr: u64) -> bool {
let shift: u32 = 4; let branch_idx = ((branch_addr >> shift) as usize) % self.btb_size;
let target_idx = ((target_addr >> shift) as usize) % self.btb_size;
branch_idx == target_idx
}
pub fn recommend_offset(&self, branch_addr: u64, target_addr: u64) -> i64 {
if self.has_btb_alias(branch_addr, target_addr) {
64
} else {
0
}
}
}
pub struct LSDOptimizer {
pub available: bool,
pub max_uops: usize,
pub max_chunks: usize,
pub microarch: X86MicroArch,
}
impl LSDOptimizer {
pub fn new(microarch: X86MicroArch) -> Self {
let (available, max_uops, max_chunks) = match microarch {
X86MicroArch::SandyBridge => (true, 28, 8),
X86MicroArch::Haswell => (true, 56, 8),
X86MicroArch::Skylake => (true, 64, 8),
X86MicroArch::IceLake => (true, 70, 8),
_ => (false, 0, 0),
};
Self {
available,
max_uops,
max_chunks,
microarch,
}
}
pub fn loop_fits_in_lsd(
&self,
uop_count: usize,
estimated_bytes: usize,
contains_calls: bool,
has_mismatched_stack: bool,
) -> bool {
if !self.available {
return false;
}
if contains_calls {
return false;
}
if has_mismatched_stack {
return false;
}
if uop_count > self.max_uops {
return false;
}
let chunks = (estimated_bytes + 15) / 16;
if chunks > self.max_chunks {
return false;
}
true
}
pub fn make_lsd_friendly(
&self,
uop_count: &mut usize,
_estimated_bytes: &mut usize,
) -> Vec<String> {
let mut recommendations = Vec::new();
if *uop_count > self.max_uops {
recommendations.push(format!(
"Loop has {} μops, LSD capacity is {}. Consider loop distribution.",
*uop_count, self.max_uops
));
}
recommendations
}
}
pub struct ZenLoopBuffer {
pub available: bool,
pub capacity: usize,
pub max_loop_instructions: usize,
}
impl ZenLoopBuffer {
pub fn new(microarch: X86MicroArch) -> Self {
match microarch {
X86MicroArch::Zen1 | X86MicroArch::Zen2 => Self {
available: true,
capacity: 4096,
max_loop_instructions: 256,
},
X86MicroArch::Zen3 | X86MicroArch::Zen4 => Self {
available: true,
capacity: 4096,
max_loop_instructions: 512,
},
X86MicroArch::Zen5 => Self {
available: true,
capacity: 6750,
max_loop_instructions: 1024,
},
_ => Self {
available: false,
capacity: 0,
max_loop_instructions: 0,
},
}
}
pub fn loop_fits_in_op_cache(&self, uop_count: usize) -> bool {
self.available && uop_count <= self.capacity
}
}
pub struct PrefetchDistanceCalculator {
pub l1_latency: u32,
pub l2_latency: u32,
pub l3_latency: u32,
pub dram_latency: u32,
pub cache_line_size: u32,
}
impl Default for PrefetchDistanceCalculator {
fn default() -> Self {
Self {
l1_latency: 4,
l2_latency: 12,
l3_latency: 44,
dram_latency: 200,
cache_line_size: 64,
}
}
}
impl PrefetchDistanceCalculator {
pub fn new(microarch: X86MicroArch) -> Self {
match microarch {
X86MicroArch::Skylake => Self {
l1_latency: 5,
l2_latency: 14,
l3_latency: 42,
dram_latency: 180,
cache_line_size: 64,
},
X86MicroArch::IceLake => Self {
l1_latency: 5,
l2_latency: 13,
l3_latency: 55,
dram_latency: 160,
cache_line_size: 64,
},
X86MicroArch::AlderLakeP => Self {
l1_latency: 5,
l2_latency: 15,
l3_latency: 50,
dram_latency: 150,
cache_line_size: 64,
},
X86MicroArch::Zen3 => Self {
l1_latency: 4,
l2_latency: 12,
l3_latency: 46,
dram_latency: 180,
cache_line_size: 64,
},
X86MicroArch::Zen4 => Self {
l1_latency: 4,
l2_latency: 12,
l3_latency: 50,
dram_latency: 160,
cache_line_size: 64,
},
X86MicroArch::Zen5 => Self {
l1_latency: 4,
l2_latency: 14,
l3_latency: 48,
dram_latency: 140,
cache_line_size: 64,
},
_ => Self::default(),
}
}
pub fn compute_prefetch_distance(
&self,
bytes_per_iteration: u32,
target_cache_level: PrefetchType,
) -> u32 {
let latency = match target_cache_level {
PrefetchType::T0 => self.l1_latency,
PrefetchType::T1 => self.l2_latency,
PrefetchType::T2 => self.l3_latency,
PrefetchType::NTA => self.dram_latency,
};
if bytes_per_iteration == 0 {
return 1;
}
let distance = (latency as u64 * bytes_per_iteration as u64 + self.cache_line_size as u64
- 1)
/ self.cache_line_size as u64;
distance.max(1).min(1024) as u32
}
pub fn is_prefetch_beneficial(
&self,
trip_count: &TripCountEstimate,
bytes_per_iteration: u32,
) -> bool {
match trip_count {
TripCountEstimate::Exact(n) => *n >= 8 && bytes_per_iteration >= 16,
TripCountEstimate::Max(n) => *n >= 16 && bytes_per_iteration >= 32,
TripCountEstimate::Symbolic(_) => bytes_per_iteration >= 64,
TripCountEstimate::Unknown => bytes_per_iteration >= 64,
}
}
}
pub struct LoopAnalysisUtil;
impl LoopAnalysisUtil {
pub fn block_frequency(block: BlockId, header: BlockId, loop_blocks: &[BlockId]) -> f64 {
if block == header {
return 1.0;
}
if !loop_blocks.contains(&block) {
return 0.0;
}
0.8
}
pub fn is_hot_loop(trip_count: &TripCountEstimate, body_size: usize) -> bool {
match trip_count {
TripCountEstimate::Exact(n) => *n >= 10 || (*n as usize * body_size) >= 100,
TripCountEstimate::Max(n) => *n >= 20,
TripCountEstimate::Unknown => body_size >= 50,
TripCountEstimate::Symbolic(_) => true,
}
}
pub fn compatible_iteration_spaces(a: &X86NaturalLoop, b: &X86NaturalLoop) -> bool {
match (&a.trip_count, &b.trip_count) {
(TripCountEstimate::Exact(ta), TripCountEstimate::Exact(tb)) => ta == tb,
(TripCountEstimate::Max(_), TripCountEstimate::Max(_)) => {
true
}
_ => false,
}
}
pub fn is_counting_loop(loop_info: &X86NaturalLoop) -> bool {
!loop_info.induction_vars.is_empty()
&& loop_info.induction_vars.iter().any(|iv| iv.is_basic)
}
pub fn nest_frequency(loop_info: &X86NaturalLoop, all_loops: &[X86NaturalLoop]) -> f64 {
let mut freq: f64 = 1.0;
let mut current = Some(loop_info.id);
while let Some(lid) = current {
let parent = all_loops.iter().find(|l| l.id == lid);
if let Some(parent_loop) = parent {
match &parent_loop.trip_count {
TripCountEstimate::Exact(n) => freq *= *n as f64,
TripCountEstimate::Max(n) => freq *= *n as f64,
_ => freq *= 100.0, }
current = parent_loop.parent;
} else {
break;
}
}
freq
}
}
fn float_loop_might_be_infinite(loop_info: &X86NaturalLoop) -> bool {
if loop_info.exit_edges.is_empty() {
return true;
}
if loop_info.trip_count == TripCountEstimate::Unknown && !loop_info.is_reducible {
return true;
}
false
}
pub struct LoopDependenceAnalyzer {
pub distance_vectors: Vec<DependenceVector>,
pub has_loop_independent_deps: bool,
}
#[derive(Debug, Clone)]
pub struct DependenceVector {
pub source: ValueId,
pub target: ValueId,
pub directions: Vec<DependenceDirection>,
pub distances: Vec<Option<i64>>,
pub dep_type: DependenceType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DependenceDirection {
Negative,
Zero,
Positive,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DependenceType {
Flow,
Anti,
Output,
Input,
}
impl LoopDependenceAnalyzer {
pub fn new() -> Self {
Self {
distance_vectors: Vec::new(),
has_loop_independent_deps: false,
}
}
pub fn analyze_loop(
&mut self,
_loop_info: &X86NaturalLoop,
_blocks: &HashMap<BlockId, Vec<ValueId>>,
) {
self.has_loop_independent_deps = false;
}
pub fn can_parallelize(&self) -> bool {
self.distance_vectors.is_empty() && !self.has_loop_independent_deps
}
pub fn can_interchange(&self) -> bool {
for dv in &self.distance_vectors {
let has_negative = dv
.directions
.iter()
.any(|d| *d == DependenceDirection::Negative);
if has_negative {
return false;
}
}
true
}
}
#[derive(Debug, Clone)]
pub struct LoopCost {
pub uops: usize,
pub instructions: usize,
pub branches: usize,
pub cycles_per_iter: f64,
pub memory_ops: usize,
pub fp_ops: usize,
pub fits_in_uop_cache: bool,
pub fits_in_lsd: bool,
}
impl LoopCost {
pub fn estimate(loop_info: &X86NaturalLoop, cost_config: &X86LoopCostConfig) -> Self {
let uops = loop_info.uop_count;
let instructions = loop_info.body_size;
let branches = loop_info.blocks.len(); let memory_ops = if loop_info.contains_memory_ops {
instructions / 4
} else {
0
};
let fp_ops = instructions / 8;
let theoretical_cycles = uops as f64 / 4.0;
let adjusted_cycles = theoretical_cycles * 1.15;
let fits_in_uop_cache = uops <= cost_config.uop_cache_capacity;
let fits_in_lsd = cost_config.lsd_available && uops <= cost_config.lsd_capacity;
Self {
uops,
instructions,
branches,
cycles_per_iter: adjusted_cycles,
memory_ops,
fp_ops,
fits_in_uop_cache,
fits_in_lsd,
}
}
pub fn total_cost(&self, trip_count: u64) -> f64 {
self.cycles_per_iter * trip_count as f64
}
}
pub struct X86LoopOptimizerPass {
pub optimizer: X86LoopOptimizer,
pub enabled: bool,
pub priority: u32,
pub name: String,
}
impl X86LoopOptimizerPass {
pub fn new(subtarget: X86Subtarget) -> Self {
Self {
optimizer: X86LoopOptimizer::new(subtarget),
enabled: true,
priority: 50,
name: "x86-loop-optimizer".to_string(),
}
}
pub fn run(
&mut self,
func: &ValueRef,
blocks: &HashMap<BlockId, Vec<ValueId>>,
pred_map: &HashMap<BlockId, Vec<BlockId>>,
succ_map: &HashMap<BlockId, Vec<BlockId>>,
) -> bool {
if !self.enabled {
return false;
}
let stats = self
.optimizer
.run_pipeline(func, blocks, pred_map, succ_map);
stats.made_progress()
}
pub fn stats(&self) -> &X86LoopOptStats {
&self.optimizer.stats
}
pub fn disable(&mut self) {
self.enabled = false;
}
pub fn enable(&mut self) {
self.enabled = true;
}
}
pub struct X86TuningPresets;
impl X86TuningPresets {
pub fn skylake() -> X86LoopCostConfig {
X86LoopCostConfig::for_microarch(X86MicroArch::Skylake)
}
pub fn ice_lake() -> X86LoopCostConfig {
X86LoopCostConfig::for_microarch(X86MicroArch::IceLake)
}
pub fn alder_lake_p() -> X86LoopCostConfig {
X86LoopCostConfig::for_microarch(X86MicroArch::AlderLakeP)
}
pub fn zen3() -> X86LoopCostConfig {
X86LoopCostConfig::for_microarch(X86MicroArch::Zen3)
}
pub fn zen4() -> X86LoopCostConfig {
X86LoopCostConfig::for_microarch(X86MicroArch::Zen4)
}
pub fn zen5() -> X86LoopCostConfig {
X86LoopCostConfig::for_microarch(X86MicroArch::Zen5)
}
pub fn conservative() -> X86LoopCostConfig {
X86LoopCostConfig {
max_unroll_factor: 4,
max_full_unroll_insts: 50,
max_partial_unroll_insts: 50,
max_unroll_jam_factor: 2,
max_predication_body_size: 10,
min_trip_count_for_unroll: 8,
max_trip_count_full_unroll: 16,
min_body_size_strength_reduce: 5,
max_loop_depth_interchange: 2,
loop_alignment: 16,
align_loop_headers: false,
insert_prefetches: false,
l1_cache_line_size: 64,
l2_cache_line_size: 64,
uop_cache_capacity: 1024,
lsd_available: false,
lsd_capacity: 0,
}
}
pub fn aggressive() -> X86LoopCostConfig {
X86LoopCostConfig {
max_unroll_factor: 16,
max_full_unroll_insts: 500,
max_partial_unroll_insts: 200,
max_unroll_jam_factor: 8,
max_predication_body_size: 30,
min_trip_count_for_unroll: 2,
max_trip_count_full_unroll: 512,
min_body_size_strength_reduce: 2,
max_loop_depth_interchange: 4,
loop_alignment: 64,
align_loop_headers: true,
insert_prefetches: true,
l1_cache_line_size: 64,
l2_cache_line_size: 64,
uop_cache_capacity: 8192,
lsd_available: true,
lsd_capacity: 1024,
}
}
}
pub struct X86LoopPeeler {
pub max_peel_count: u32,
pub peel_for_alignment: bool,
pub target_alignment: u32,
pub peeled_count: usize,
}
impl X86LoopPeeler {
pub fn new() -> Self {
Self {
max_peel_count: 16,
peel_for_alignment: true,
target_alignment: 64,
peeled_count: 0,
}
}
pub fn compute_peel_count(
&self,
trip_count: &TripCountEstimate,
unroll_factor: u32,
misalignment_bytes: u32,
bytes_per_iter: u32,
) -> u32 {
let mut peel = 0u32;
if let TripCountEstimate::Exact(n) = trip_count {
let remainder = (*n as u32) % unroll_factor;
if remainder > 0 && remainder <= self.max_peel_count {
peel = peel.max(remainder);
}
}
if self.peel_for_alignment && misalignment_bytes > 0 && bytes_per_iter > 0 {
let align_peel =
(self.target_alignment - misalignment_bytes + bytes_per_iter - 1) / bytes_per_iter;
if align_peel <= self.max_peel_count {
peel = peel.max(align_peel);
}
}
peel.min(self.max_peel_count)
}
pub fn is_peeling_beneficial(&self, trip_count: &TripCountEstimate, body_size: usize) -> bool {
match trip_count {
TripCountEstimate::Exact(n) if *n >= 8 => body_size >= 3,
TripCountEstimate::Max(n) if *n >= 16 => body_size >= 5,
_ => false,
}
}
pub fn peel_loop(&mut self, optimizer: &mut X86LoopOptimizer, loop_idx: usize) -> bool {
if loop_idx >= optimizer.loops.len() {
return false;
}
let loop_info = &optimizer.loops[loop_idx];
let unroll_factor = optimizer.cost_config.max_unroll_factor;
let peel_count = self.compute_peel_count(
&loop_info.trip_count,
unroll_factor,
0, 4, );
if peel_count > 0 {
self.peeled_count += 1;
return true;
}
false
}
}
impl Default for X86LoopPeeler {
fn default() -> Self {
Self::new()
}
}
pub struct X86LoopTiler {
pub l1_cache_size: usize,
pub l2_cache_size: usize,
pub max_tile_dim: usize,
pub min_tile_dim: usize,
}
impl X86LoopTiler {
pub fn new(microarch: X86MicroArch) -> Self {
let (l1, l2) = match microarch {
X86MicroArch::Skylake | X86MicroArch::IceLake => (32768, 262144),
X86MicroArch::AlderLakeP => (49152, 1310720),
X86MicroArch::Zen3 => (32768, 524288),
X86MicroArch::Zen4 => (32768, 1048576),
X86MicroArch::Zen5 => (49152, 1048576),
_ => (32768, 262144),
};
Self {
l1_cache_size: l1,
l2_cache_size: l2,
max_tile_dim: 512,
min_tile_dim: 8,
}
}
pub fn compute_tile_size(&self, element_size: usize, num_arrays: usize, use_l2: bool) -> usize {
let cache_size = if use_l2 {
self.l2_cache_size
} else {
self.l1_cache_size
};
let tile_bytes = cache_size / (num_arrays.max(2));
let tile_elements = tile_bytes / element_size;
let tile_dim = (tile_elements as f64).sqrt() as usize;
tile_dim.max(self.min_tile_dim).min(self.max_tile_dim)
}
pub fn is_tiling_applicable(&self, outer: &X86NaturalLoop, inner: &X86NaturalLoop) -> bool {
match (&outer.trip_count, &inner.trip_count) {
(TripCountEstimate::Exact(o), TripCountEstimate::Exact(i)) if *o >= 16 && *i >= 16 => {
true
}
_ => false,
}
}
pub fn tile_bounds(&self, trip_count: u64, tile_size: u64) -> Vec<(u64, u64)> {
let mut bounds = Vec::new();
let mut start: u64 = 0;
while start < trip_count {
let end = (start + tile_size).min(trip_count);
bounds.push((start, end));
start = end;
}
bounds
}
}
pub struct X86LoopGuardOptimizer {
pub guards_optimized: usize,
pub guards_widened: usize,
pub guards_eliminated: usize,
}
impl X86LoopGuardOptimizer {
pub fn new() -> Self {
Self {
guards_optimized: 0,
guards_widened: 0,
guards_eliminated: 0,
}
}
pub fn analyze_guard(&mut self, loop_info: &X86NaturalLoop) -> Option<GuardAnalysisResult> {
if loop_info.preheader.is_none() {
return None;
}
match &loop_info.trip_count {
TripCountEstimate::Exact(0) => Some(GuardAnalysisResult::DeadLoop),
TripCountEstimate::Exact(1) => Some(GuardAnalysisResult::SingleIteration),
_ => Some(GuardAnalysisResult::Viable),
}
}
pub fn widen_guard(&mut self, _loop_info: &X86NaturalLoop, min_trip_count: u64) -> bool {
if min_trip_count > 1 {
self.guards_widened += 1;
return true;
}
false
}
pub fn eliminate_guard(&mut self) {
self.guards_eliminated += 1;
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GuardAnalysisResult {
DeadLoop,
SingleIteration,
Viable,
Unknown,
}
pub struct X86LoopCodegenPatterns {
pub microarch: X86MicroArch,
}
impl X86LoopCodegenPatterns {
pub fn new(microarch: X86MicroArch) -> Self {
Self { microarch }
}
pub fn loop_counter_pattern(&self) -> LoopCounterPattern {
if self.microarch as u32 >= X86MicroArch::SandyBridge as u32 {
LoopCounterPattern::DecJccFused
} else {
LoopCounterPattern::DecJcc
}
}
pub fn address_update_pattern(&self) -> AddressUpdatePattern {
match self.microarch {
X86MicroArch::SandyBridge
| X86MicroArch::Haswell
| X86MicroArch::Skylake
| X86MicroArch::IceLake
| X86MicroArch::AlderLakeP => AddressUpdatePattern::Lea,
X86MicroArch::Zen1
| X86MicroArch::Zen2
| X86MicroArch::Zen3
| X86MicroArch::Zen4
| X86MicroArch::Zen5 => AddressUpdatePattern::LeaAgu,
_ => AddressUpdatePattern::Add,
}
}
pub fn simd_profitable(&self, trip_count: &TripCountEstimate, element_size: u32) -> bool {
let vector_width = match self.microarch {
X86MicroArch::AlderLakeP
| X86MicroArch::IceLake
| X86MicroArch::Zen4
| X86MicroArch::Zen5
| X86MicroArch::GraniteRapids => 512,
_ => 256,
};
let elements_per_vector = vector_width / (element_size * 8);
match trip_count.as_bound() {
Some(n) => n >= elements_per_vector as u64 * 2,
None => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopCounterPattern {
DecJcc,
DecJccFused,
SubJcc,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AddressUpdatePattern {
Add,
Lea,
LeaAgu,
}
pub struct X86LoopScheduler {
pub num_ports: usize,
pub port0_fp_mul: bool,
pub port1_fp_add: bool,
pub port5_shuffle: bool,
pub port6_branch: bool,
}
impl X86LoopScheduler {
pub fn new(microarch: X86MicroArch) -> Self {
match microarch {
X86MicroArch::Skylake | X86MicroArch::IceLake => Self {
num_ports: 8,
port0_fp_mul: true,
port1_fp_add: true,
port5_shuffle: true,
port6_branch: true,
},
X86MicroArch::Zen3 | X86MicroArch::Zen4 => Self {
num_ports: 10,
port0_fp_mul: true,
port1_fp_add: true,
port5_shuffle: true,
port6_branch: false, },
X86MicroArch::Zen5 => Self {
num_ports: 12,
port0_fp_mul: true,
port1_fp_add: true,
port5_shuffle: true,
port6_branch: false,
},
_ => Self {
num_ports: 4,
port0_fp_mul: true,
port1_fp_add: true,
port5_shuffle: false,
port6_branch: false,
},
}
}
pub fn estimate_cycles(&self, alu_uops: usize, mem_uops: usize, branch_uops: usize) -> f64 {
let total_uops = alu_uops + mem_uops + branch_uops;
let effective_ports = if self.port6_branch && branch_uops > 0 {
self.num_ports as f64
} else {
(self.num_ports - 1) as f64
};
total_uops as f64 / effective_ports
}
pub fn is_port_balanced(&self, port_pressure: &[usize]) -> bool {
if port_pressure.is_empty() {
return true;
}
let max_pressure = port_pressure.iter().max().copied().unwrap_or(1);
let min_pressure = port_pressure.iter().min().copied().unwrap_or(0);
min_pressure > 0 && (max_pressure as f64 / min_pressure as f64) <= 1.5
}
}
pub struct X86LoopBufferAnalysis {
pub microarch: X86MicroArch,
pub dsb_capacity: usize,
pub lsd_capacity: usize,
pub has_dsb: bool,
pub has_lsd: bool,
}
impl X86LoopBufferAnalysis {
pub fn new(microarch: X86MicroArch) -> Self {
let has_dsb = microarch.has_uop_cache();
let has_lsd = microarch.has_lsd();
Self {
microarch,
dsb_capacity: microarch.uop_cache_size(),
lsd_capacity: if has_lsd {
microarch.lsd_issue_width() * 18
} else {
0
},
has_dsb,
has_lsd,
}
}
pub fn classify_fit(&self, uop_count: usize) -> LoopBufferFit {
if self.has_lsd && uop_count <= self.lsd_capacity {
return LoopBufferFit::FitsInLSD;
}
if self.has_dsb && uop_count <= self.dsb_capacity {
return LoopBufferFit::FitsInDSB;
}
if uop_count <= self.dsb_capacity / 2 {
return LoopBufferFit::PartiallyInDSB;
}
LoopBufferFit::ExceedsAllBuffers
}
pub fn buffer_recommendations(&self, uop_count: usize) -> Vec<String> {
let mut recs = Vec::new();
let fit = self.classify_fit(uop_count);
match fit {
LoopBufferFit::FitsInLSD => {
recs.push("Loop fits in LSD — ensure no mismatched stack ops".to_string());
}
LoopBufferFit::FitsInDSB => {
recs.push("Loop fits in DSB — consider unrolling to fill LSD".to_string());
}
LoopBufferFit::ExceedsAllBuffers => {
recs.push(format!(
"Loop has {} μops, exceeds all buffers. Consider loop distribution.",
uop_count
));
}
LoopBufferFit::PartiallyInDSB => {
recs.push("Loop partially fits in DSB — may have front-end bubbles".to_string());
}
}
recs
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopBufferFit {
FitsInLSD,
FitsInDSB,
PartiallyInDSB,
ExceedsAllBuffers,
}
pub struct LoopExitProfiler {
pub hot_exit: Option<BlockId>,
pub cold_exits: Vec<BlockId>,
pub hot_exit_probability: f64,
}
impl LoopExitProfiler {
pub fn new() -> Self {
Self {
hot_exit: None,
cold_exits: Vec::new(),
hot_exit_probability: 1.0,
}
}
pub fn analyze_exits(&mut self, loop_info: &X86NaturalLoop) {
if loop_info.exit_edges.is_empty() {
return;
}
if loop_info.exit_edges.len() == 1 {
self.hot_exit = Some(loop_info.exit_edges[0].1);
self.hot_exit_probability = 1.0;
} else {
self.hot_exit = Some(loop_info.exit_edges[0].1);
self.hot_exit_probability = 0.9;
for edge in loop_info.exit_edges.iter().skip(1) {
self.cold_exits.push(edge.1);
}
}
}
pub fn is_cold_exit(&self, block: BlockId) -> bool {
self.cold_exits.contains(&block)
}
pub fn exit_branch_layout(&self, from: BlockId, to: BlockId) -> BranchLayout {
if Some(to) == self.hot_exit {
BranchLayout::FallThrough
} else if self.is_cold_exit(to) {
BranchLayout::JumpCold
} else {
BranchLayout::Default
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BranchLayout {
FallThrough,
JumpCold,
Default,
}
pub struct X86LoopHintHandler {
pub pragma_unroll: HashMap<u64, u32>,
pub pragma_nounroll: HashSet<u64>,
pub pragma_full_unroll: HashSet<u64>,
pub pragma_vectorize: HashMap<u64, bool>,
pub pgo_hot_threshold: f64,
pub pgo_cold_threshold: f64,
}
impl X86LoopHintHandler {
pub fn new() -> Self {
Self {
pragma_unroll: HashMap::new(),
pragma_nounroll: HashSet::new(),
pragma_full_unroll: HashSet::new(),
pragma_vectorize: HashMap::new(),
pgo_hot_threshold: 0.9,
pgo_cold_threshold: 0.1,
}
}
pub fn parse_unroll_pragma(&mut self, loop_id: u64, factor: u32) {
if factor == 0 {
self.pragma_nounroll.insert(loop_id);
} else if factor == 1 {
self.pragma_full_unroll.insert(loop_id);
} else {
self.pragma_unroll.insert(loop_id, factor);
}
}
pub fn parse_vectorize_pragma(&mut self, loop_id: u64, enable: bool) {
self.pragma_vectorize.insert(loop_id, enable);
}
pub fn get_unroll_hint(&self, loop_id: u64) -> Option<u32> {
if self.pragma_nounroll.contains(&loop_id) {
return Some(0);
}
if self.pragma_full_unroll.contains(&loop_id) {
return Some(1);
}
self.pragma_unroll.get(&loop_id).copied()
}
pub fn is_vectorize_enabled(&self, loop_id: u64) -> Option<bool> {
self.pragma_vectorize.get(&loop_id).copied()
}
pub fn is_hot_from_pgo(&self, execution_count: f64, total_count: f64) -> bool {
if total_count == 0.0 {
return false;
}
let ratio = execution_count / total_count;
ratio >= self.pgo_hot_threshold
}
pub fn is_cold_from_pgo(&self, execution_count: f64, total_count: f64) -> bool {
if total_count == 0.0 {
return false;
}
let ratio = execution_count / total_count;
ratio <= self.pgo_cold_threshold
}
}
pub struct X86CacheMissEstimator {
pub l1_miss_penalty: u32,
pub l2_miss_penalty: u32,
pub l3_miss_penalty: u32,
pub dram_penalty: u32,
}
impl X86CacheMissEstimator {
pub fn new(microarch: X86MicroArch) -> Self {
match microarch {
X86MicroArch::Skylake => Self {
l1_miss_penalty: 10,
l2_miss_penalty: 20,
l3_miss_penalty: 50,
dram_penalty: 200,
},
X86MicroArch::IceLake => Self {
l1_miss_penalty: 10,
l2_miss_penalty: 18,
l3_miss_penalty: 60,
dram_penalty: 180,
},
X86MicroArch::Zen4 => Self {
l1_miss_penalty: 8,
l2_miss_penalty: 16,
l3_miss_penalty: 55,
dram_penalty: 160,
},
X86MicroArch::Zen5 => Self {
l1_miss_penalty: 8,
l2_miss_penalty: 16,
l3_miss_penalty: 50,
dram_penalty: 140,
},
_ => Self {
l1_miss_penalty: 12,
l2_miss_penalty: 24,
l3_miss_penalty: 60,
dram_penalty: 250,
},
}
}
pub fn estimate_miss_penalty(
&self,
trip_count: u64,
bytes_per_iter: u64,
cache_line_size: u64,
) -> f64 {
let cache_lines_per_iter = (bytes_per_iter + cache_line_size - 1) / cache_line_size;
let cold_misses = cache_lines_per_iter.min(trip_count);
let capacity_misses = if bytes_per_iter * trip_count > 32768 {
(bytes_per_iter * trip_count / 32768).max(1)
} else {
0
};
(cold_misses as f64 * self.l1_miss_penalty as f64)
+ (capacity_misses as f64 * self.l3_miss_penalty as f64 * 0.1)
}
pub fn compare_layouts(
&self,
layout_a_misses: f64,
layout_b_misses: f64,
layout_a_cycles: f64,
layout_b_cycles: f64,
) -> i32 {
let total_a = layout_a_cycles + layout_a_misses;
let total_b = layout_b_cycles + layout_b_misses;
if total_a < total_b {
-1
} else if total_a > total_b {
1
} else {
0
}
}
}
pub struct X86LoopPipelineStage {
pub name: String,
#[allow(dead_code)]
is_mir_pass: bool,
pub order: u32,
pub enabled_transforms: Vec<LoopTransformType>,
}
impl Clone for X86LoopPipelineStage {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
is_mir_pass: self.is_mir_pass,
order: self.order,
enabled_transforms: self.enabled_transforms.clone(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LoopTransformType {
Rotate,
FullUnroll,
PartialUnroll,
UnrollAndJam,
Fuse,
Distribute,
Interchange,
Unswitch,
IdiomRecognize,
Delete,
Simplify,
StrengthReduce,
Reroll,
Version,
Predicate,
Peel,
Tile,
Align,
Prefetch,
}
impl LoopTransformType {
pub fn name(&self) -> &'static str {
match self {
LoopTransformType::Rotate => "rotate",
LoopTransformType::FullUnroll => "full-unroll",
LoopTransformType::PartialUnroll => "partial-unroll",
LoopTransformType::UnrollAndJam => "unroll-and-jam",
LoopTransformType::Fuse => "fuse",
LoopTransformType::Distribute => "distribute",
LoopTransformType::Interchange => "interchange",
LoopTransformType::Unswitch => "unswitch",
LoopTransformType::IdiomRecognize => "idiom-recognize",
LoopTransformType::Delete => "delete",
LoopTransformType::Simplify => "simplify",
LoopTransformType::StrengthReduce => "strength-reduce",
LoopTransformType::Reroll => "reroll",
LoopTransformType::Version => "version",
LoopTransformType::Predicate => "predicate",
LoopTransformType::Peel => "peel",
LoopTransformType::Tile => "tile",
LoopTransformType::Align => "align",
LoopTransformType::Prefetch => "prefetch",
}
}
}
impl X86LoopPipelineStage {
pub fn new(name: &str, order: u32, is_mir_pass: bool) -> Self {
Self {
name: name.to_string(),
is_mir_pass,
order,
enabled_transforms: Vec::new(),
}
}
pub fn with_transform(mut self, transform: LoopTransformType) -> Self {
self.enabled_transforms.push(transform);
self
}
pub fn run(&self, optimizer: &mut X86LoopOptimizer) -> usize {
let mut count = 0usize;
for transform in &self.enabled_transforms {
match transform {
LoopTransformType::Rotate => {
count += optimizer.run_loop_rotation();
}
LoopTransformType::Simplify => {
count += optimizer.run_loop_simplify();
}
LoopTransformType::Delete => {
count += optimizer.run_loop_deletion();
}
LoopTransformType::IdiomRecognize => {
count += optimizer.run_idiom_recognition();
}
LoopTransformType::FullUnroll | LoopTransformType::PartialUnroll => {
count += optimizer.run_loop_unrolling();
}
LoopTransformType::UnrollAndJam => {
count += optimizer.run_unroll_and_jam();
}
LoopTransformType::Fuse => {
count += optimizer.run_loop_fusion();
}
LoopTransformType::Distribute => {
count += optimizer.run_loop_distribution();
}
LoopTransformType::Interchange => {
optimizer.run_loop_interchange();
count += optimizer.stats.interchanged;
}
LoopTransformType::Unswitch => {
count += optimizer.run_loop_unswitching();
}
LoopTransformType::StrengthReduce => {
count += optimizer.run_strength_reduction();
}
LoopTransformType::Reroll => {
count += optimizer.run_loop_rerolling();
}
LoopTransformType::Version => {
count += optimizer.run_loop_versioning();
}
LoopTransformType::Predicate => {
count += optimizer.run_loop_predication();
}
LoopTransformType::Align
| LoopTransformType::Prefetch
| LoopTransformType::Peel
| LoopTransformType::Tile => {
count += optimizer.run_x86_tuning();
}
}
}
count
}
}
pub struct X86LoopPipelineBuilder {
pub optimizer: X86LoopOptimizer,
pub stages: Vec<X86LoopPipelineStage>,
}
impl X86LoopPipelineBuilder {
pub fn new(subtarget: X86Subtarget) -> Self {
Self {
optimizer: X86LoopOptimizer::new(subtarget),
stages: Vec::new(),
}
}
pub fn build_standard_pipeline(&mut self) {
self.stages.clear();
self.stages.push(
X86LoopPipelineStage::new("loop-canonicalize", 10, false)
.with_transform(LoopTransformType::Simplify)
.with_transform(LoopTransformType::Rotate),
);
self.stages.push(
X86LoopPipelineStage::new("loop-cleanup", 20, false)
.with_transform(LoopTransformType::Delete)
.with_transform(LoopTransformType::IdiomRecognize),
);
self.stages.push(
X86LoopPipelineStage::new("loop-hl-transform", 30, false)
.with_transform(LoopTransformType::Interchange)
.with_transform(LoopTransformType::Fuse)
.with_transform(LoopTransformType::Distribute)
.with_transform(LoopTransformType::Unswitch),
);
self.stages.push(
X86LoopPipelineStage::new("loop-arith", 40, false)
.with_transform(LoopTransformType::StrengthReduce),
);
self.stages.push(
X86LoopPipelineStage::new("loop-unroll", 50, false)
.with_transform(LoopTransformType::FullUnroll)
.with_transform(LoopTransformType::PartialUnroll)
.with_transform(LoopTransformType::UnrollAndJam),
);
self.stages.push(
X86LoopPipelineStage::new("loop-spec", 60, false)
.with_transform(LoopTransformType::Version)
.with_transform(LoopTransformType::Predicate),
);
self.stages.push(
X86LoopPipelineStage::new("loop-size", 70, false)
.with_transform(LoopTransformType::Reroll),
);
self.stages.push(
X86LoopPipelineStage::new("loop-x86-tune", 80, true)
.with_transform(LoopTransformType::Align)
.with_transform(LoopTransformType::Prefetch),
);
}
pub fn build_aggressive_pipeline(&mut self) {
self.build_standard_pipeline();
self.stages.insert(
2,
X86LoopPipelineStage::new("loop-peel-tile", 25, false)
.with_transform(LoopTransformType::Peel)
.with_transform(LoopTransformType::Tile),
);
}
pub fn build_size_pipeline(&mut self) {
self.stages.clear();
self.stages.push(
X86LoopPipelineStage::new("loop-canonicalize", 10, false)
.with_transform(LoopTransformType::Simplify)
.with_transform(LoopTransformType::Rotate),
);
self.stages.push(
X86LoopPipelineStage::new("loop-size", 20, false)
.with_transform(LoopTransformType::Reroll)
.with_transform(LoopTransformType::Delete),
);
}
pub fn run_on_function(
&mut self,
func: &ValueRef,
blocks: &HashMap<BlockId, Vec<ValueId>>,
pred_map: &HashMap<BlockId, Vec<BlockId>>,
succ_map: &HashMap<BlockId, Vec<BlockId>>,
) -> &X86LoopOptStats {
self.optimizer
.detect_loops(func, blocks, pred_map, succ_map);
self.optimizer.compute_scev_for_loops(func);
self.optimizer.analyze_induction_vars();
self.stages.sort_by_key(|s| s.order);
for stage in &self.stages.clone() {
let _count = stage.run(&mut self.optimizer);
}
self.optimizer.loops_transformed = self.optimizer.count_transformations();
&self.optimizer.stats
}
pub fn stats(&self) -> &X86LoopOptStats {
&self.optimizer.stats
}
pub fn reset(&mut self) {
self.optimizer.reset();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_subtarget() -> X86Subtarget {
X86Subtarget::default_64bit()
}
fn make_simple_cfg() -> (
ValueRef,
HashMap<BlockId, Vec<ValueId>>,
HashMap<BlockId, Vec<BlockId>>,
HashMap<BlockId, Vec<BlockId>>,
) {
let func = ValueRef::new_function("test_func");
let entry_id: BlockId = 1;
let body_id: BlockId = 2;
let exit_id: BlockId = 3;
let mut blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
blocks.insert(entry_id, vec![]);
blocks.insert(body_id, vec![]);
blocks.insert(exit_id, vec![]);
let mut pred_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
pred_map.insert(entry_id, vec![]);
pred_map.insert(body_id, vec![entry_id, body_id]); pred_map.insert(exit_id, vec![body_id]);
let mut succ_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
succ_map.insert(entry_id, vec![body_id]);
succ_map.insert(body_id, vec![exit_id, body_id]); succ_map.insert(exit_id, vec![]);
(func, blocks, pred_map, succ_map)
}
fn make_loop_cfg() -> (
ValueRef,
HashMap<BlockId, Vec<ValueId>>,
HashMap<BlockId, Vec<BlockId>>,
HashMap<BlockId, Vec<BlockId>>,
) {
let func = ValueRef::new_function("loop_func");
let preheader_id: BlockId = 1;
let header_id: BlockId = 2;
let body_id: BlockId = 3;
let latch_id: BlockId = 4;
let exit_id: BlockId = 5;
let mut blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
blocks.insert(preheader_id, vec![]);
blocks.insert(header_id, vec![]);
blocks.insert(body_id, vec![]);
blocks.insert(latch_id, vec![]);
blocks.insert(exit_id, vec![]);
let mut pred_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
pred_map.insert(preheader_id, vec![]);
pred_map.insert(header_id, vec![preheader_id, latch_id]);
pred_map.insert(body_id, vec![header_id]);
pred_map.insert(latch_id, vec![body_id]);
pred_map.insert(exit_id, vec![latch_id]);
let mut succ_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
succ_map.insert(preheader_id, vec![header_id]);
succ_map.insert(header_id, vec![body_id]);
succ_map.insert(body_id, vec![latch_id]);
succ_map.insert(latch_id, vec![header_id, exit_id]);
succ_map.insert(exit_id, vec![]);
(func, blocks, pred_map, succ_map)
}
fn make_nested_loop_cfg() -> (
ValueRef,
HashMap<BlockId, Vec<ValueId>>,
HashMap<BlockId, Vec<BlockId>>,
HashMap<BlockId, Vec<BlockId>>,
) {
let func = ValueRef::new_function("nested_func");
let outer_preheader: BlockId = 1;
let outer_header: BlockId = 2;
let inner_preheader: BlockId = 3;
let inner_header: BlockId = 4;
let inner_latch: BlockId = 5;
let outer_latch: BlockId = 6;
let exit: BlockId = 7;
let mut blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
for i in 1..=7 {
blocks.insert(i, vec![]);
}
let mut pred_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
pred_map.insert(outer_preheader, vec![]);
pred_map.insert(outer_header, vec![outer_preheader, outer_latch]);
pred_map.insert(inner_preheader, vec![outer_header]);
pred_map.insert(inner_header, vec![inner_preheader, inner_latch]);
pred_map.insert(inner_latch, vec![inner_header]);
pred_map.insert(outer_latch, vec![inner_header]);
pred_map.insert(exit, vec![outer_latch]);
let mut succ_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
succ_map.insert(outer_preheader, vec![outer_header]);
succ_map.insert(outer_header, vec![inner_preheader]);
succ_map.insert(inner_preheader, vec![inner_header]);
succ_map.insert(inner_header, vec![inner_latch, outer_latch]);
succ_map.insert(inner_latch, vec![inner_header]);
succ_map.insert(outer_latch, vec![outer_header, exit]);
succ_map.insert(exit, vec![]);
(func, blocks, pred_map, succ_map)
}
#[test]
fn test_microarch_lsd_detection() {
assert!(X86MicroArch::SandyBridge.has_lsd());
assert!(X86MicroArch::Haswell.has_lsd());
assert!(X86MicroArch::Skylake.has_lsd());
assert!(X86MicroArch::IceLake.has_lsd());
assert!(!X86MicroArch::AlderLakeP.has_lsd());
assert!(!X86MicroArch::Zen3.has_lsd());
assert!(!X86MicroArch::Zen4.has_lsd());
assert!(!X86MicroArch::Generic.has_lsd());
}
#[test]
fn test_microarch_uop_cache() {
assert_eq!(X86MicroArch::Skylake.uop_cache_size(), 1536);
assert_eq!(X86MicroArch::AlderLakeP.uop_cache_size(), 4096);
assert_eq!(X86MicroArch::Zen4.uop_cache_size(), 4096);
assert_eq!(X86MicroArch::Zen5.uop_cache_size(), 6750);
}
#[test]
fn test_microarch_decode_width() {
assert_eq!(X86MicroArch::Skylake.decode_width(), 4);
assert_eq!(X86MicroArch::IceLake.decode_width(), 5);
assert_eq!(X86MicroArch::AlderLakeP.decode_width(), 6);
assert_eq!(X86MicroArch::Zen5.decode_width(), 8);
}
#[test]
fn test_microarch_btb_entries() {
assert_eq!(X86MicroArch::Skylake.btb_entries(), 5120);
assert_eq!(X86MicroArch::AlderLakeP.btb_entries(), 12288);
}
#[test]
fn test_microarch_preferred_alignment() {
assert_eq!(X86MicroArch::Skylake.preferred_loop_alignment(), 16);
assert_eq!(X86MicroArch::IceLake.preferred_loop_alignment(), 32);
assert_eq!(X86MicroArch::Zen5.preferred_loop_alignment(), 64);
}
#[test]
fn test_trip_count_exact() {
let tc = TripCountEstimate::Exact(10);
assert_eq!(tc.as_exact(), Some(10));
assert_eq!(tc.as_bound(), Some(10));
assert!(tc.has_bound());
assert!(tc.is_exact());
}
#[test]
fn test_trip_count_max() {
let tc = TripCountEstimate::Max(100);
assert_eq!(tc.as_exact(), None);
assert_eq!(tc.as_bound(), Some(100));
assert!(tc.has_bound());
assert!(!tc.is_exact());
}
#[test]
fn test_trip_count_unknown() {
let tc = TripCountEstimate::Unknown;
assert_eq!(tc.as_exact(), None);
assert_eq!(tc.as_bound(), None);
assert!(!tc.has_bound());
assert!(!tc.is_exact());
}
#[test]
fn test_trip_count_symbolic() {
let tc = TripCountEstimate::Symbolic("N".to_string());
assert_eq!(tc.as_exact(), None);
assert_eq!(tc.as_bound(), None);
assert!(!tc.has_bound());
assert!(!tc.is_exact());
}
#[test]
fn test_trip_count_display() {
assert_eq!(format!("{}", TripCountEstimate::Exact(5)), "exact(5)");
assert_eq!(format!("{}", TripCountEstimate::Max(20)), "max(20)");
assert_eq!(
format!("{}", TripCountEstimate::Symbolic("N".to_string())),
"symbolic(N)"
);
assert_eq!(format!("{}", TripCountEstimate::Unknown), "unknown");
}
#[test]
fn test_induction_var_basic() {
let iv = InductionVariable::new_basic(100, 0, 1, 42, 32);
assert!(iv.is_basic);
assert_eq!(iv.start, 0);
assert_eq!(iv.step, 1);
assert_eq!(iv.loop_id, 42);
assert_eq!(iv.bit_width, 32);
assert_eq!(iv.at_iteration(0), 0);
assert_eq!(iv.at_iteration(5), 5);
assert_eq!(iv.at_iteration(10), 10);
}
#[test]
fn test_induction_var_derived() {
let iv = InductionVariable::new_derived(200, 100, 8, 0, 42, 64);
assert!(!iv.is_basic);
assert_eq!(iv.base_iv, Some(100));
assert_eq!(iv.step, 8);
}
#[test]
fn test_induction_var_at_iteration() {
let iv = InductionVariable::new_basic(1, 10, 3, 0, 32);
assert_eq!(iv.at_iteration(0), 10);
assert_eq!(iv.at_iteration(1), 13);
assert_eq!(iv.at_iteration(5), 25);
}
#[test]
fn test_cost_config_default() {
let config = X86LoopCostConfig::default();
assert_eq!(config.max_unroll_factor, 8);
assert_eq!(config.loop_alignment, 16);
assert_eq!(config.l1_cache_line_size, 64);
assert!(config.lsd_available);
}
#[test]
fn test_cost_config_skylake() {
let config = X86LoopCostConfig::for_microarch(X86MicroArch::Skylake);
assert_eq!(config.loop_alignment, 16);
assert_eq!(config.uop_cache_capacity, 1536);
assert!(config.lsd_available);
}
#[test]
fn test_cost_config_icelake() {
let config = X86LoopCostConfig::for_microarch(X86MicroArch::IceLake);
assert_eq!(config.loop_alignment, 32);
assert_eq!(config.uop_cache_capacity, 2304);
}
#[test]
fn test_cost_config_zen5() {
let config = X86LoopCostConfig::for_microarch(X86MicroArch::Zen5);
assert_eq!(config.loop_alignment, 64);
assert_eq!(config.max_unroll_factor, 10);
assert!(!config.lsd_available);
}
#[test]
fn test_cost_config_should_full_unroll_small_loop() {
let config = X86LoopCostConfig::default();
let tc = TripCountEstimate::Exact(4);
assert!(config.should_full_unroll(&tc, 10, false));
}
#[test]
fn test_cost_config_should_not_full_unroll_large_loop() {
let config = X86LoopCostConfig::default();
let tc = TripCountEstimate::Exact(100);
assert!(!config.should_full_unroll(&tc, 100, false));
}
#[test]
fn test_cost_config_should_not_full_unroll_with_calls() {
let config = X86LoopCostConfig::default();
let tc = TripCountEstimate::Exact(2);
assert!(!config.should_full_unroll(&tc, 5, true));
}
#[test]
fn test_cost_config_compute_unroll_factor() {
let config = X86LoopCostConfig::default();
let tc = TripCountEstimate::Exact(32);
let factor = config.compute_unroll_factor(&tc, 20);
assert!(factor >= 1);
assert!(factor <= config.max_unroll_factor);
}
#[test]
fn test_cost_config_compute_unroll_factor_small_body() {
let config = X86LoopCostConfig::default();
let tc = TripCountEstimate::Exact(16);
let factor = config.compute_unroll_factor(&tc, 5);
assert!(factor >= 2); }
#[test]
fn test_optimizer_new() {
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
assert!(optimizer.loops.is_empty());
assert_eq!(optimizer.loops_analyzed, 0);
assert_eq!(optimizer.loops_transformed, 0);
assert!(!optimizer.debug_trace);
}
#[test]
fn test_optimizer_with_cost_config() {
let subtarget = make_test_subtarget();
let config = X86LoopCostConfig::conservative();
let optimizer = X86LoopOptimizer::with_cost_config(subtarget, config);
assert_eq!(optimizer.cost_config.max_unroll_factor, 4);
}
#[test]
fn test_optimizer_reset() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
optimizer.loops_analyzed = 5;
optimizer.loops_transformed = 3;
optimizer.reset();
assert_eq!(optimizer.loops_analyzed, 0);
assert_eq!(optimizer.loops_transformed, 0);
assert!(optimizer.loops.is_empty());
}
#[test]
fn test_detect_loops_empty_function() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let func = ValueRef::new_function("empty");
let blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
let pred_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
let succ_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
let loops = optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
assert!(loops.is_empty());
assert_eq!(optimizer.loops_analyzed, 0);
}
#[test]
fn test_detect_loops_simple_cfg() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_simple_cfg();
let loops = optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
assert!(!loops.is_empty(), "Expected at least one loop detected");
}
#[test]
fn test_detect_loops_with_loop() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
let loops = optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
assert!(!loops.is_empty());
if !loops.is_empty() {
let loop_info = &loops[0];
assert_eq!(loop_info.header, 2); assert!(loop_info.blocks.contains(&2));
assert!(loop_info.blocks.contains(&3)); assert!(loop_info.blocks.contains(&4)); }
}
#[test]
fn test_detect_loops_nested() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_nested_loop_cfg();
let loops = optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
assert!(
loops.len() >= 2,
"Expected at least 2 loops, found {}",
loops.len()
);
}
#[test]
fn test_loop_detection_reducible_loop() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
let loops = optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
if !loops.is_empty() {
assert!(loops[0].is_reducible);
}
}
#[test]
fn test_loop_exit_info() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
let loops = optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
if !loops.is_empty() {
assert!(
!loops[0].exit_edges.is_empty(),
"Loop should have exit edges"
);
}
}
#[test]
fn test_loop_nesting_computation() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_nested_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
let has_nested = optimizer.loops.iter().any(|l| l.depth > 0);
assert!(
has_nested || optimizer.loops.len() < 2,
"Nested loops should have depth > 0"
);
}
#[test]
fn test_loops_by_depth() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_nested_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
let sorted = optimizer.loops_by_depth();
if sorted.len() >= 2 {
assert!(sorted[0].depth >= sorted[sorted.len() - 1].depth);
}
}
#[test]
fn test_is_loop_header() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
if !optimizer.loops.is_empty() {
let header = optimizer.loops[0].header;
assert!(optimizer.is_loop_header(header));
}
}
#[test]
fn test_trip_count_estimation_default() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
if !optimizer.loops.is_empty() {
assert!(
matches!(optimizer.loops[0].trip_count, TripCountEstimate::Unknown)
|| optimizer.loops[0].trip_count.has_bound()
);
}
}
#[test]
fn test_invariant_detector_creation() {
let detector = LoopInvariantDetector::new();
assert!(detector.loop_invariants.is_empty());
assert!(detector.conservative_memory);
}
#[test]
fn test_invariant_detector_detect() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
let mut detector = LoopInvariantDetector::new();
detector.detect_invariants(&mut optimizer, &blocks);
assert_eq!(detector.loop_invariants.len(), optimizer.loops.len());
}
#[test]
fn test_analyze_induction_vars() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
let count = optimizer.analyze_induction_vars();
assert!(count > 0 || optimizer.loops.is_empty());
}
#[test]
fn test_loop_rotation_noop_when_single_latch() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
let rotated = optimizer.run_loop_rotation();
assert!(rotated <= optimizer.loops.len());
}
#[test]
fn test_loop_simplify() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
let simplified = optimizer.run_loop_simplify();
assert!(simplified <= optimizer.loops.len());
}
#[test]
fn test_loop_unrolling_no_effect_on_unknown_trip_count() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
let unrolled = optimizer.run_loop_unrolling();
assert_eq!(optimizer.stats.fully_unrolled, 0);
}
#[test]
fn test_full_unroll_with_known_trip_count() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
optimizer.cost_config.max_trip_count_full_unroll = 16;
optimizer.cost_config.max_full_unroll_insts = 200;
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
if !optimizer.loops.is_empty() {
optimizer.loops[0].trip_count = TripCountEstimate::Exact(4);
optimizer.loops[0].body_size = 5;
}
optimizer.run_loop_unrolling();
assert!(optimizer.stats.fully_unrolled <= optimizer.loops.len());
}
#[test]
fn test_loop_distribution_noop_small_loop() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
let distributed = optimizer.run_loop_distribution();
assert_eq!(distributed, 0);
}
#[test]
fn test_loop_fusion_noop_without_adjacent_loops() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_simple_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
let fused = optimizer.run_loop_fusion();
assert_eq!(fused, 0);
}
#[test]
fn test_loop_deletion_noop_for_live_loops() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
let deleted = optimizer.run_loop_deletion();
if !optimizer.loops.is_empty() && optimizer.loops[0].contains_memory_ops {
assert_eq!(deleted, 0);
}
}
#[test]
fn test_idiom_recognition_noop_on_empty() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
optimizer.run_idiom_recognition();
assert_eq!(optimizer.stats.idioms_recognized, 0);
}
#[test]
fn test_memset_recognition_heuristic() {
let loop_info = X86NaturalLoop {
id: 0,
header: 1,
blocks: vec![1],
preheader: Some(0),
latches: vec![1],
exiting_blocks: vec![1],
exit_blocks: vec![2],
exit_edges: vec![(1, 2)],
back_edges: vec![(1, 1)],
depth: 0,
parent: None,
children: vec![],
is_reducible: true,
trip_count: TripCountEstimate::Exact(100),
body_size: 3,
uop_count: 4,
contains_calls: false,
contains_memory_ops: true,
is_vectorizable: false,
invariants: vec![],
induction_vars: vec![InductionVariable::new_basic(1, 0, 8, 0, 64)],
is_canonical: true,
canonical_latch: Some(1),
};
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
assert!(optimizer.try_recognize_memset(&loop_info));
}
#[test]
fn test_x86_tuning_runs() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
let tunings = optimizer.run_x86_tuning();
assert!(tunings >= 0);
}
#[test]
fn test_align_loop_header() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
if !optimizer.loops.is_empty() {
let aligned = optimizer.align_loop_header(0);
assert!(aligned == 0 || aligned == 1);
}
}
#[test]
fn test_nop_padding() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
if !optimizer.loops.is_empty() {
let padded = optimizer.insert_nop_padding(0);
assert!(padded <= 1);
}
}
#[test]
fn test_prefetch_insertion_for_large_loop() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
if !optimizer.loops.is_empty() {
optimizer.loops[0].trip_count = TripCountEstimate::Exact(100);
optimizer.loops[0].contains_memory_ops = true;
optimizer.loops[0].body_size = 10;
let prefetches = optimizer.insert_prefetch_instructions(0);
assert!(prefetches >= 1);
}
}
#[test]
fn test_nop_generator_small_padding() {
let mut r#gen = X86NopGenerator::new(X86MicroArch::Skylake);
let nops = r#gen.generate_nops(1);
assert_eq!(nops.len(), 1);
assert_eq!(nops[0], 0x90);
}
#[test]
fn test_nop_generator_medium_padding() {
let mut r#gen = X86NopGenerator::new(X86MicroArch::Skylake);
let nops = r#gen.generate_nops(7);
assert_eq!(nops.len(), 7);
assert!(nops[0] == 0x0F);
}
#[test]
fn test_nop_generator_large_padding() {
let mut r#gen = X86NopGenerator::new(X86MicroArch::IceLake);
let nops = r#gen.generate_nops(15);
assert_eq!(nops.len(), 15);
}
#[test]
fn test_nop_generator_zero_padding() {
let mut r#gen = X86NopGenerator::new(X86MicroArch::Skylake);
let nops = r#gen.generate_nops(0);
assert!(nops.is_empty());
}
#[test]
fn test_nop_compute_padding() {
assert_eq!(X86NopGenerator::compute_padding(0, 16), 0);
assert_eq!(X86NopGenerator::compute_padding(1, 16), 15);
assert_eq!(X86NopGenerator::compute_padding(16, 16), 0);
assert_eq!(X86NopGenerator::compute_padding(31, 32), 1);
assert_eq!(X86NopGenerator::compute_padding(64, 64), 0);
}
#[test]
fn test_btb_optimizer_creation() {
let btb = X86BTBOptimizer::new(X86MicroArch::Skylake);
assert_eq!(btb.btb_size, 5120);
assert_eq!(btb.btb_associativity, 4);
assert!(btb.has_indirect_branch_prediction);
}
#[test]
fn test_btb_has_alias() {
let btb = X86BTBOptimizer::new(X86MicroArch::Skylake);
assert!(btb.has_btb_alias(0x1000, 0x1000));
}
#[test]
fn test_btb_recommend_offset() {
let btb = X86BTBOptimizer::new(X86MicroArch::Skylake);
let offset = btb.recommend_offset(0x1000, 0x1000);
assert_eq!(offset, 64);
}
#[test]
fn test_lsd_optimizer_skylake() {
let lsd = LSDOptimizer::new(X86MicroArch::Skylake);
assert!(lsd.available);
assert_eq!(lsd.max_uops, 64);
}
#[test]
fn test_lsd_optimizer_zen3_not_available() {
let lsd = LSDOptimizer::new(X86MicroArch::Zen3);
assert!(!lsd.available);
}
#[test]
fn test_lsd_fits_small_loop() {
let lsd = LSDOptimizer::new(X86MicroArch::Skylake);
assert!(lsd.loop_fits_in_lsd(20, 50, false, false));
}
#[test]
fn test_lsd_does_not_fit_large_loop() {
let lsd = LSDOptimizer::new(X86MicroArch::Skylake);
assert!(!lsd.loop_fits_in_lsd(100, 500, false, false));
}
#[test]
fn test_lsd_rejects_calls() {
let lsd = LSDOptimizer::new(X86MicroArch::Haswell);
assert!(!lsd.loop_fits_in_lsd(10, 30, true, false));
}
#[test]
fn test_zen_loop_buffer_zen4() {
let buf = ZenLoopBuffer::new(X86MicroArch::Zen4);
assert!(buf.available);
assert_eq!(buf.capacity, 4096);
}
#[test]
fn test_zen_loop_buffer_zen5() {
let buf = ZenLoopBuffer::new(X86MicroArch::Zen5);
assert_eq!(buf.capacity, 6750);
}
#[test]
fn test_zen_loop_buffer_not_available_on_intel() {
let buf = ZenLoopBuffer::new(X86MicroArch::Skylake);
assert!(!buf.available);
}
#[test]
fn test_zen_loop_buffer_fits() {
let buf = ZenLoopBuffer::new(X86MicroArch::Zen4);
assert!(buf.loop_fits_in_op_cache(1000));
assert!(!buf.loop_fits_in_op_cache(5000));
}
#[test]
fn test_prefetch_calculator_defaults() {
let calc = PrefetchDistanceCalculator::default();
assert_eq!(calc.cache_line_size, 64);
assert_eq!(calc.l1_latency, 4);
}
#[test]
fn test_prefetch_calculator_skylake() {
let calc = PrefetchDistanceCalculator::new(X86MicroArch::Skylake);
assert_eq!(calc.l2_latency, 14);
assert_eq!(calc.l3_latency, 42);
}
#[test]
fn test_compute_prefetch_distance() {
let calc = PrefetchDistanceCalculator::default();
let distance = calc.compute_prefetch_distance(64, PrefetchType::T0);
assert!(distance >= 1);
}
#[test]
fn test_compute_prefetch_distance_large_stride() {
let calc = PrefetchDistanceCalculator::default();
let distance = calc.compute_prefetch_distance(256, PrefetchType::NTA);
assert!(distance >= 4);
}
#[test]
fn test_compute_prefetch_distance_zero_bytes() {
let calc = PrefetchDistanceCalculator::default();
let distance = calc.compute_prefetch_distance(0, PrefetchType::T0);
assert_eq!(distance, 1);
}
#[test]
fn test_is_prefetch_beneficial() {
let calc = PrefetchDistanceCalculator::default();
let tc = TripCountEstimate::Exact(10);
assert!(calc.is_prefetch_beneficial(&tc, 64));
}
#[test]
fn test_is_prefetch_not_beneficial_small_loop() {
let calc = PrefetchDistanceCalculator::default();
let tc = TripCountEstimate::Exact(3);
assert!(!calc.is_prefetch_beneficial(&tc, 8));
}
#[test]
fn test_prefetch_type_mnemonics() {
assert_eq!(PrefetchType::T0.mnemonic_suffix(), "t0");
assert_eq!(PrefetchType::T1.mnemonic_suffix(), "t1");
assert_eq!(PrefetchType::T2.mnemonic_suffix(), "t2");
assert_eq!(PrefetchType::NTA.mnemonic_suffix(), "nta");
}
#[test]
fn test_dependence_analyzer_empty() {
let analyzer = LoopDependenceAnalyzer::new();
assert!(analyzer.can_parallelize());
assert!(analyzer.can_interchange());
}
#[test]
fn test_dependence_direction_values() {
let dv = DependenceVector {
source: 1,
target: 2,
directions: vec![DependenceDirection::Positive],
distances: vec![Some(1)],
dep_type: DependenceType::Flow,
};
assert_eq!(dv.dep_type, DependenceType::Flow);
assert_eq!(dv.distances[0], Some(1));
}
#[test]
fn test_loop_cost_estimation() {
let loop_info = X86NaturalLoop {
id: 0,
header: 1,
blocks: vec![1, 2],
preheader: None,
latches: vec![2],
exiting_blocks: vec![2],
exit_blocks: vec![3],
exit_edges: vec![(2, 3)],
back_edges: vec![(2, 1)],
depth: 0,
parent: None,
children: vec![],
is_reducible: true,
trip_count: TripCountEstimate::Exact(10),
body_size: 20,
uop_count: 24,
contains_calls: false,
contains_memory_ops: true,
is_vectorizable: false,
invariants: vec![],
induction_vars: vec![],
is_canonical: false,
canonical_latch: None,
};
let config = X86LoopCostConfig::default();
let cost = LoopCost::estimate(&loop_info, &config);
assert_eq!(cost.uops, 24);
assert_eq!(cost.instructions, 20);
assert_eq!(cost.branches, 2);
assert!(cost.cycles_per_iter > 0.0);
}
#[test]
fn test_loop_cost_total_cost() {
let cost = LoopCost {
uops: 20,
instructions: 15,
branches: 1,
cycles_per_iter: 5.0,
memory_ops: 3,
fp_ops: 0,
fits_in_uop_cache: true,
fits_in_lsd: true,
};
let total = cost.total_cost(10);
assert_eq!(total, 50.0);
}
#[test]
fn test_x86_scev_creation() {
let scev = X86SCEV::new(X86MicroArch::Skylake);
assert!(!scev.is_add_rec(&SCEV::Constant(42)));
}
#[test]
fn test_x86_scev_add_rec() {
let scev = X86SCEV::new(X86MicroArch::Skylake);
let add_rec = SCEV::AddRec {
base: Box::new(SCEV::Constant(0)),
step: Box::new(SCEV::Constant(1)),
loop_header: 42,
is_signed: true,
};
assert!(scev.is_add_rec(&add_rec));
}
#[test]
fn test_trip_count_from_exit_constant() {
let scev = X86SCEV::new(X86MicroArch::Skylake);
let add_rec = SCEV::AddRec {
base: Box::new(SCEV::Constant(0)),
step: Box::new(SCEV::Constant(1)),
loop_header: 1,
is_signed: true,
};
let bound = SCEV::Constant(10);
let tc = scev.trip_count_from_exit(&add_rec, &bound, ICmpPred::Slt);
assert_eq!(tc, Some(TripCountEstimate::Exact(10)));
}
#[test]
fn test_trip_count_from_exit_sle() {
let scev = X86SCEV::new(X86MicroArch::Skylake);
let add_rec = SCEV::AddRec {
base: Box::new(SCEV::Constant(0)),
step: Box::new(SCEV::Constant(1)),
loop_header: 1,
is_signed: true,
};
let bound = SCEV::Constant(9);
let tc = scev.trip_count_from_exit(&add_rec, &bound, ICmpPred::Sle);
assert_eq!(tc, Some(TripCountEstimate::Exact(10)));
}
#[test]
fn test_alignment_policy_microarch() {
assert_eq!(
LoopAlignmentPolicy::for_microarch(X86MicroArch::Skylake),
LoopAlignmentPolicy::Align16
);
assert_eq!(
LoopAlignmentPolicy::for_microarch(X86MicroArch::IceLake),
LoopAlignmentPolicy::Align32
);
assert_eq!(
LoopAlignmentPolicy::for_microarch(X86MicroArch::Zen5),
LoopAlignmentPolicy::Align64
);
}
#[test]
fn test_alignment_policy_bytes() {
assert_eq!(LoopAlignmentPolicy::None.alignment_bytes(), 1);
assert_eq!(LoopAlignmentPolicy::Align16.alignment_bytes(), 16);
assert_eq!(LoopAlignmentPolicy::Align32.alignment_bytes(), 32);
assert_eq!(LoopAlignmentPolicy::Align64.alignment_bytes(), 64);
}
#[test]
fn test_is_counting_loop() {
let loop_info = X86NaturalLoop {
id: 0,
header: 1,
blocks: vec![1],
preheader: None,
latches: vec![1],
exiting_blocks: vec![1],
exit_blocks: vec![2],
exit_edges: vec![(1, 2)],
back_edges: vec![(1, 1)],
depth: 0,
parent: None,
children: vec![],
is_reducible: true,
trip_count: TripCountEstimate::Unknown,
body_size: 5,
uop_count: 6,
contains_calls: false,
contains_memory_ops: false,
is_vectorizable: false,
invariants: vec![],
induction_vars: vec![InductionVariable::new_basic(1, 0, 1, 0, 32)],
is_canonical: false,
canonical_latch: None,
};
assert!(LoopAnalysisUtil::is_counting_loop(&loop_info));
}
#[test]
fn test_is_hot_loop() {
assert!(LoopAnalysisUtil::is_hot_loop(
&TripCountEstimate::Exact(10),
5
));
assert!(LoopAnalysisUtil::is_hot_loop(
&TripCountEstimate::Max(20),
10
));
assert!(!LoopAnalysisUtil::is_hot_loop(
&TripCountEstimate::Exact(2),
3
));
}
#[test]
fn test_pass_manager_creation() {
let subtarget = make_test_subtarget();
let pass = X86LoopOptimizerPass::new(subtarget);
assert!(pass.enabled);
assert_eq!(pass.priority, 50);
assert_eq!(pass.name, "x86-loop-optimizer");
}
#[test]
fn test_pass_manager_run() {
let subtarget = make_test_subtarget();
let mut pass = X86LoopOptimizerPass::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
let made_progress = pass.run(&func, &blocks, &pred_map, &succ_map);
assert!(made_progress || !made_progress);
}
#[test]
fn test_pass_manager_disable() {
let subtarget = make_test_subtarget();
let mut pass = X86LoopOptimizerPass::new(subtarget);
pass.disable();
assert!(!pass.enabled);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
let made_progress = pass.run(&func, &blocks, &pred_map, &succ_map);
assert!(!made_progress);
}
#[test]
fn test_pass_manager_enable() {
let subtarget = make_test_subtarget();
let mut pass = X86LoopOptimizerPass::new(subtarget);
pass.disable();
pass.enable();
assert!(pass.enabled);
}
#[test]
fn test_tuning_presets_skylake() {
let config = X86TuningPresets::skylake();
assert_eq!(config.loop_alignment, 16);
}
#[test]
fn test_tuning_presets_zen5() {
let config = X86TuningPresets::zen5();
assert_eq!(config.max_unroll_factor, 10);
assert_eq!(config.max_unroll_jam_factor, 8);
}
#[test]
fn test_tuning_presets_conservative() {
let config = X86TuningPresets::conservative();
assert_eq!(config.max_unroll_factor, 4);
assert!(!config.align_loop_headers);
assert!(!config.insert_prefetches);
}
#[test]
fn test_tuning_presets_aggressive() {
let config = X86TuningPresets::aggressive();
assert_eq!(config.max_unroll_factor, 16);
assert!(config.align_loop_headers);
assert!(config.insert_prefetches);
}
#[test]
fn test_stats_default_zero() {
let stats = X86LoopOptStats::default();
assert!(!stats.made_progress());
}
#[test]
fn test_stats_made_progress() {
let mut stats = X86LoopOptStats::new();
stats.loops_rotated = 1;
assert!(stats.made_progress());
}
#[test]
fn test_stats_merge() {
let mut a = X86LoopOptStats::new();
a.loops_rotated = 2;
a.fully_unrolled = 1;
let mut b = X86LoopOptStats::new();
b.loops_rotated = 3;
b.simplified = 4;
a.merge(&b);
assert_eq!(a.loops_rotated, 5);
assert_eq!(a.fully_unrolled, 1);
assert_eq!(a.simplified, 4);
}
#[test]
fn test_should_partial_unroll_zero_uops() {
let config = X86LoopCostConfig::default();
let tc = TripCountEstimate::Exact(8);
assert!(config.should_partial_unroll(&tc, 0, false));
}
#[test]
fn test_should_partial_unroll_with_calls() {
let config = X86LoopCostConfig::default();
let tc = TripCountEstimate::Exact(10);
assert!(!config.should_partial_unroll(&tc, 50, true));
}
#[test]
fn test_should_partial_unroll_below_threshold() {
let config = X86LoopCostConfig::default();
let tc = TripCountEstimate::Exact(3); assert!(!config.should_partial_unroll(&tc, 20, false));
}
#[test]
fn test_compute_unroll_factor_unknown() {
let config = X86LoopCostConfig::default();
let tc = TripCountEstimate::Unknown;
assert_eq!(config.compute_unroll_factor(&tc, 10), 1);
}
#[test]
fn test_should_full_unroll_unknown() {
let config = X86LoopCostConfig::default();
assert!(!config.should_full_unroll(&TripCountEstimate::Unknown, 5, false));
}
#[test]
fn test_default_loop_not_canonical() {
let loop_info = X86NaturalLoop {
id: 0,
header: 1,
blocks: vec![1],
preheader: None,
latches: vec![],
exiting_blocks: vec![],
exit_blocks: vec![],
exit_edges: vec![],
back_edges: vec![],
depth: 0,
parent: None,
children: vec![],
is_reducible: false,
trip_count: TripCountEstimate::Unknown,
body_size: 0,
uop_count: 0,
contains_calls: false,
contains_memory_ops: false,
is_vectorizable: false,
invariants: vec![],
induction_vars: vec![],
is_canonical: false,
canonical_latch: None,
};
assert!(!loop_info.is_canonical);
assert!(loop_info.invariants.is_empty());
assert!(loop_info.induction_vars.is_empty());
}
#[test]
fn test_full_pipeline_runs() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
let stats = optimizer.run_pipeline(&func, &blocks, &pred_map, &succ_map);
assert!(optimizer.loops_analyzed > 0 || optimizer.loops.is_empty());
assert!(optimizer.loops_transformed >= 0);
}
#[test]
fn test_full_pipeline_nested() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_nested_loop_cfg();
let stats = optimizer.run_pipeline(&func, &blocks, &pred_map, &succ_map);
assert!(optimizer.loops_analyzed >= 2);
}
#[test]
fn test_full_pipeline_empty_function() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let func = ValueRef::new_function("empty");
let blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
let pred_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
let succ_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
let stats = optimizer.run_pipeline(&func, &blocks, &pred_map, &succ_map);
assert!(optimizer.loops_analyzed == 0);
assert!(!stats.made_progress());
}
#[test]
fn test_interchange_noop_without_nested_loops() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
optimizer.run_loop_interchange();
assert_eq!(optimizer.stats.interchanged, 0);
}
#[test]
fn test_versioning_runs_with_memory_ops() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
if !optimizer.loops.is_empty() {
optimizer.loops[0].contains_memory_ops = true;
optimizer.loops[0].body_size = 5;
}
optimizer.run_loop_versioning();
assert!(optimizer.stats.versioned <= optimizer.loops.len());
}
#[test]
fn test_predication_runs_for_small_loops() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
if !optimizer.loops.is_empty() {
optimizer.loops[0].body_size = 5;
optimizer.loops[0].contains_memory_ops = true;
}
optimizer.run_loop_predication();
assert!(optimizer.stats.predicated <= optimizer.loops.len());
}
#[test]
fn test_rerolling_large_body_small_trip() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
optimizer.run_loop_rerolling();
assert_eq!(optimizer.stats.rerolled, 0);
}
#[test]
fn test_count_transformations_aggregate() {
let mut stats = X86LoopOptStats::new();
stats.loops_rotated = 1;
stats.fully_unrolled = 2;
stats.simplified = 3;
let mut optimizer = X86LoopOptimizer::new(make_test_subtarget());
optimizer.stats = stats;
let total = optimizer.count_transformations();
assert_eq!(total, 6);
}
#[test]
fn test_loop_might_be_infinite_no_exits() {
let loop_info = X86NaturalLoop {
id: 0,
header: 1,
blocks: vec![1],
preheader: None,
latches: vec![1],
exiting_blocks: vec![],
exit_blocks: vec![],
exit_edges: vec![],
back_edges: vec![(1, 1)],
depth: 0,
parent: None,
children: vec![],
is_reducible: true,
trip_count: TripCountEstimate::Unknown,
body_size: 5,
uop_count: 6,
contains_calls: false,
contains_memory_ops: false,
is_vectorizable: false,
invariants: vec![],
induction_vars: vec![],
is_canonical: false,
canonical_latch: None,
};
assert!(float_loop_might_be_infinite(&loop_info));
}
#[test]
fn test_loop_not_infinite_with_exits() {
let loop_info = X86NaturalLoop {
id: 0,
header: 1,
blocks: vec![1, 2],
preheader: Some(0),
latches: vec![2],
exiting_blocks: vec![2],
exit_blocks: vec![3],
exit_edges: vec![(2, 3)],
back_edges: vec![(2, 1)],
depth: 0,
parent: None,
children: vec![],
is_reducible: true,
trip_count: TripCountEstimate::Exact(10),
body_size: 5,
uop_count: 6,
contains_calls: false,
contains_memory_ops: false,
is_vectorizable: false,
invariants: vec![],
induction_vars: vec![],
is_canonical: true,
canonical_latch: Some(2),
};
assert!(!float_loop_might_be_infinite(&loop_info));
}
#[test]
fn test_choose_prefetch_type() {
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert_eq!(optimizer.choose_prefetch_type(8, false), PrefetchType::T0);
}
#[test]
fn test_x86_scev_decompose() {
let scev = X86SCEV::new(X86MicroArch::Skylake);
let add_rec = SCEV::AddRec {
base: Box::new(SCEV::Constant(0)),
step: Box::new(SCEV::Constant(2)),
loop_header: 99,
is_signed: false,
};
let (base, step, lh) = scev.decompose_add_rec(&add_rec).unwrap();
assert_eq!(*base, SCEV::Constant(0));
assert_eq!(*step, SCEV::Constant(2));
assert_eq!(lh, 99);
}
#[test]
fn test_strength_reduction_runs() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
optimizer.analyze_induction_vars();
optimizer.run_strength_reduction();
assert!(optimizer.stats.strength_reduced <= optimizer.loops.len());
}
#[test]
fn test_unswitch_with_invariants() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
if !optimizer.loops.is_empty() {
optimizer.loops[0].invariants = vec![100, 200];
optimizer.loops[0].trip_count = TripCountEstimate::Exact(20);
}
optimizer.run_loop_unswitching();
assert!(optimizer.stats.unswitched <= optimizer.loops.len());
}
#[test]
fn test_unroll_and_jam_with_child_loops() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_nested_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
for l in &mut optimizer.loops {
l.trip_count = TripCountEstimate::Exact(4);
}
optimizer.run_unroll_and_jam();
}
#[test]
fn test_lsd_make_friendly() {
let lsd = LSDOptimizer::new(X86MicroArch::Haswell);
let recommendations = lsd.make_lsd_friendly(&mut 100, &mut 200);
assert!(!recommendations.is_empty());
}
#[test]
fn test_block_frequency() {
let freq = LoopAnalysisUtil::block_frequency(1, 1, &[1, 2, 3]);
assert_eq!(freq, 1.0);
}
#[test]
fn test_block_frequency_non_header() {
let freq = LoopAnalysisUtil::block_frequency(2, 1, &[1, 2, 3]);
assert!(freq > 0.0);
}
#[test]
fn test_block_frequency_outside() {
let freq = LoopAnalysisUtil::block_frequency(99, 1, &[1, 2, 3]);
assert_eq!(freq, 0.0);
}
#[test]
fn test_compatible_exact() {
let a = X86NaturalLoop {
trip_count: TripCountEstimate::Exact(10),
..create_minimal_loop()
};
let b = X86NaturalLoop {
trip_count: TripCountEstimate::Exact(10),
..create_minimal_loop()
};
assert!(LoopAnalysisUtil::compatible_iteration_spaces(&a, &b));
}
#[test]
fn test_incompatible_exact() {
let a = X86NaturalLoop {
trip_count: TripCountEstimate::Exact(10),
..create_minimal_loop()
};
let b = X86NaturalLoop {
trip_count: TripCountEstimate::Exact(20),
..create_minimal_loop()
};
assert!(!LoopAnalysisUtil::compatible_iteration_spaces(&a, &b));
}
fn create_minimal_loop() -> X86NaturalLoop {
X86NaturalLoop {
id: 0,
header: 1,
blocks: vec![1],
preheader: None,
latches: vec![],
exiting_blocks: vec![],
exit_blocks: vec![],
exit_edges: vec![],
back_edges: vec![],
depth: 0,
parent: None,
children: vec![],
is_reducible: true,
trip_count: TripCountEstimate::Unknown,
body_size: 0,
uop_count: 0,
contains_calls: false,
contains_memory_ops: false,
is_vectorizable: false,
invariants: vec![],
induction_vars: vec![],
is_canonical: false,
canonical_latch: None,
}
}
#[test]
fn test_nest_frequency_single_loop() {
let loop_info = X86NaturalLoop {
trip_count: TripCountEstimate::Exact(5),
..create_minimal_loop()
};
let freq = LoopAnalysisUtil::nest_frequency(&loop_info, &[]);
assert_eq!(freq, 5.0);
}
#[test]
fn test_is_loop_dead_no_side_effects() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let loop_info = X86NaturalLoop {
id: 0,
header: 1,
blocks: vec![1],
preheader: Some(0),
latches: vec![1],
exiting_blocks: vec![1],
exit_blocks: vec![2],
exit_edges: vec![(1, 2)],
back_edges: vec![(1, 1)],
depth: 0,
parent: None,
children: vec![],
is_reducible: true,
trip_count: TripCountEstimate::Exact(10),
body_size: 3,
uop_count: 4,
contains_calls: false,
contains_memory_ops: false,
is_vectorizable: false,
invariants: vec![],
induction_vars: vec![],
is_canonical: true,
canonical_latch: Some(1),
};
optimizer.loops = vec![loop_info];
assert!(optimizer.is_loop_dead(0));
}
#[test]
fn test_is_loop_not_dead_with_calls() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let loop_info = X86NaturalLoop {
contains_calls: true,
..create_minimal_loop()
};
optimizer.loops = vec![loop_info];
assert!(!optimizer.is_loop_dead(0));
}
#[test]
fn test_is_loop_not_dead_with_memory() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let loop_info = X86NaturalLoop {
contains_memory_ops: true,
..create_minimal_loop()
};
optimizer.loops = vec![loop_info];
assert!(!optimizer.is_loop_dead(0));
}
#[test]
fn test_dependence_can_parallelize_with_no_deps() {
let analyzer = LoopDependenceAnalyzer::new();
assert!(analyzer.can_parallelize());
}
#[test]
fn test_dependence_can_interchange_with_no_negative() {
let mut analyzer = LoopDependenceAnalyzer::new();
analyzer.distance_vectors.push(DependenceVector {
source: 1,
target: 2,
directions: vec![DependenceDirection::Zero, DependenceDirection::Positive],
distances: vec![Some(0), Some(1)],
dep_type: DependenceType::Flow,
});
assert!(analyzer.can_interchange());
}
#[test]
fn test_loop_cost_fits_in_uop_cache() {
let config = X86LoopCostConfig::default();
let loop_info = X86NaturalLoop {
uop_count: 100,
body_size: 80,
blocks: vec![1, 2],
contains_memory_ops: false,
..create_minimal_loop()
};
let cost = LoopCost::estimate(&loop_info, &config);
assert!(cost.fits_in_uop_cache);
}
#[test]
fn test_loop_cost_does_not_fit_in_uop_cache() {
let config = X86LoopCostConfig {
uop_cache_capacity: 50,
..X86LoopCostConfig::default()
};
let loop_info = X86NaturalLoop {
uop_count: 100,
body_size: 80,
blocks: vec![1, 2],
contains_memory_ops: false,
..create_minimal_loop()
};
let cost = LoopCost::estimate(&loop_info, &config);
assert!(!cost.fits_in_uop_cache);
}
#[test]
fn test_debug_trace_off_by_default() {
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.debug_trace);
}
#[test]
fn test_get_loop_by_id() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
if !optimizer.loops.is_empty() {
let id = optimizer.loops[0].id;
assert!(optimizer.get_loop(id).is_some());
assert!(optimizer.get_loop(999999).is_none());
}
}
#[test]
fn test_get_innermost_loop() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_nested_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
let inner = optimizer.get_innermost_loop_for_block(4);
assert!(inner.is_some());
}
#[test]
fn test_detect_microarch() {
let subtarget = X86Subtarget::default_64bit();
let _optimizer = X86LoopOptimizer::new(subtarget);
}
#[test]
fn test_optimizer_with_skylake_cost() {
let subtarget = make_test_subtarget();
let config = X86LoopCostConfig::for_microarch(X86MicroArch::Skylake);
let optimizer = X86LoopOptimizer::with_cost_config(subtarget, config);
assert_eq!(optimizer.cost_config.loop_alignment, 16);
}
#[test]
fn test_can_distribute_large_loop() {
let loop_info = X86NaturalLoop {
body_size: 10,
contains_calls: false,
contains_memory_ops: true,
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(optimizer.can_distribute(&loop_info));
}
#[test]
fn test_cannot_distribute_small_loop() {
let loop_info = X86NaturalLoop {
body_size: 3,
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.can_distribute(&loop_info));
}
#[test]
fn test_cannot_fuse_different_trip_counts() {
let mut optimizer = X86LoopOptimizer::new(make_test_subtarget());
let mut a = create_minimal_loop();
a.trip_count = TripCountEstimate::Exact(10);
let mut b = create_minimal_loop();
b.trip_count = TripCountEstimate::Exact(20);
optimizer.loops = vec![a, b];
let _ = optimizer.run_loop_fusion();
}
#[test]
fn test_has_invariant_branches() {
let loop_info = X86NaturalLoop {
invariants: vec![1, 2],
blocks: vec![1, 2],
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(optimizer.has_invariant_branches(&loop_info));
}
#[test]
fn test_no_invariant_branches_without_invariants() {
let loop_info = X86NaturalLoop {
invariants: vec![],
blocks: vec![0],
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.has_invariant_branches(&loop_info));
}
#[test]
fn test_unswitch_profitable_high_trip_count() {
let loop_info = X86NaturalLoop {
trip_count: TripCountEstimate::Exact(20),
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(optimizer.is_profitable_to_unswitch(&loop_info));
}
#[test]
fn test_unswitch_not_profitable_low_trip_count() {
let loop_info = X86NaturalLoop {
trip_count: TripCountEstimate::Exact(3),
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.is_profitable_to_unswitch(&loop_info));
}
#[test]
fn test_can_version_for_alias() {
let loop_info = X86NaturalLoop {
contains_memory_ops: true,
induction_vars: vec![
InductionVariable::new_basic(1, 0, 1, 0, 32),
InductionVariable::new_basic(2, 0, 8, 0, 64),
],
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(optimizer.can_version_for_alias(&loop_info));
}
#[test]
fn test_can_version_for_alignment() {
let loop_info = X86NaturalLoop {
contains_memory_ops: true,
induction_vars: vec![InductionVariable::new_basic(1, 0, 1, 0, 32)],
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(optimizer.can_version_for_alignment(&loop_info));
}
#[test]
fn test_has_divergent_branches() {
let loop_info = X86NaturalLoop {
contains_memory_ops: true,
blocks: vec![1, 2],
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(optimizer.has_divergent_branches(&loop_info));
}
#[test]
fn test_no_divergent_branches_single_block() {
let loop_info = X86NaturalLoop {
contains_memory_ops: true,
blocks: vec![1],
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.has_divergent_branches(&loop_info));
}
#[test]
fn test_distribute_profitable_large_memory_loop() {
let loop_info = X86NaturalLoop {
body_size: 10,
contains_memory_ops: true,
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(optimizer.is_profitable_to_distribute(&loop_info));
}
#[test]
fn test_distribute_not_profitable_small() {
let loop_info = X86NaturalLoop {
body_size: 5,
contains_memory_ops: false,
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.is_profitable_to_distribute(&loop_info));
}
#[test]
fn test_fuse_profitable_with_memory_ops() {
let mut optimizer = X86LoopOptimizer::new(make_test_subtarget());
let mut a = create_minimal_loop();
a.contains_memory_ops = true;
a.uop_count = 10;
let mut b = create_minimal_loop();
b.uop_count = 10;
optimizer.loops = vec![a, b];
}
#[test]
fn test_interchange_profitable_with_memory_and_ivs() {
let mut optimizer = X86LoopOptimizer::new(make_test_subtarget());
let mut inner = create_minimal_loop();
inner.contains_memory_ops = true;
inner.induction_vars = vec![
InductionVariable::new_basic(1, 0, 1, 0, 32),
InductionVariable::new_basic(2, 0, 8, 0, 64),
];
optimizer.loops = vec![create_minimal_loop(), inner];
assert!(optimizer.is_profitable_to_interchange(0, 1));
}
#[test]
fn test_reroll_candidate_large_body_small_trip() {
let loop_info = X86NaturalLoop {
body_size: 30,
trip_count: TripCountEstimate::Exact(2),
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(optimizer.is_candidate_for_reroll(&loop_info));
}
#[test]
fn test_not_reroll_candidate_small_body() {
let loop_info = X86NaturalLoop {
body_size: 10,
trip_count: TripCountEstimate::Exact(10),
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.is_candidate_for_reroll(&loop_info));
}
#[test]
fn test_lsd_max_chunks_exceeded() {
let lsd = LSDOptimizer::new(X86MicroArch::SandyBridge);
assert!(!lsd.loop_fits_in_lsd(20, 200, false, false));
}
#[test]
fn test_lsd_mismatched_stack() {
let lsd = LSDOptimizer::new(X86MicroArch::Haswell);
assert!(!lsd.loop_fits_in_lsd(10, 30, false, true));
}
#[test]
fn test_all_presets_defined() {
let _ = X86TuningPresets::skylake();
let _ = X86TuningPresets::ice_lake();
let _ = X86TuningPresets::alder_lake_p();
let _ = X86TuningPresets::zen3();
let _ = X86TuningPresets::zen4();
let _ = X86TuningPresets::zen5();
let _ = X86TuningPresets::conservative();
let _ = X86TuningPresets::aggressive();
}
#[test]
fn test_all_microarchs_have_sizes() {
let archs = [
X86MicroArch::Core2,
X86MicroArch::Nehalem,
X86MicroArch::SandyBridge,
X86MicroArch::Haswell,
X86MicroArch::Skylake,
X86MicroArch::IceLake,
X86MicroArch::AlderLakeP,
X86MicroArch::AlderLakeE,
X86MicroArch::GraniteRapids,
X86MicroArch::K8,
X86MicroArch::Bulldozer,
X86MicroArch::Zen1,
X86MicroArch::Zen2,
X86MicroArch::Zen3,
X86MicroArch::Zen4,
X86MicroArch::Zen5,
X86MicroArch::Generic,
];
for arch in &archs {
let _ = arch.uop_cache_size();
let _ = arch.decode_width();
let _ = arch.btb_entries();
let _ = arch.preferred_loop_alignment();
}
}
#[test]
fn test_loop_peeling_concept() {
let config = X86LoopCostConfig::default();
let tc = TripCountEstimate::Exact(100);
let unroll_factor = config.compute_unroll_factor(&tc, 10);
let peel_count = tc.as_exact().unwrap() % unroll_factor as u64;
if peel_count > 0 {
assert!(peel_count < unroll_factor as u64);
}
}
#[test]
fn test_peel_for_alignment() {
let calc = PrefetchDistanceCalculator::default();
let bytes_per_iter: u32 = 4;
let misalignment: u32 = 12; let peel_iters = (calc.cache_line_size - misalignment) / bytes_per_iter;
assert_eq!(peel_iters, 13);
}
#[test]
fn test_tile_size_for_l1_cache() {
let l1_size: usize = 32768;
let element_size: usize = 8; let tile_dim = ((l1_size / (3 * element_size)) as f64).sqrt() as usize;
assert!(tile_dim > 0);
assert!(tile_dim * tile_dim * element_size * 3 <= l1_size);
}
#[test]
fn test_tile_size_for_l2_cache() {
let l2_size: usize = 262144; let element_size: usize = 4; let tile_dim = ((l2_size / (3 * element_size)) as f64).sqrt() as usize;
assert!(tile_dim > 50);
}
#[test]
fn test_avx512_vector_factor() {
let float_size: u32 = 4;
let vector_width: u32 = 512;
let elements = vector_width / (float_size * 8);
assert_eq!(elements, 16);
}
#[test]
fn test_avx2_vector_factor() {
let double_size: u32 = 8;
let vector_width: u32 = 256;
let elements = vector_width / (double_size * 8);
assert_eq!(elements, 4);
}
#[test]
fn test_sse_vector_factor() {
let int32_size: u32 = 4;
let vector_width: u32 = 128;
let elements = vector_width / (int32_size * 8);
assert_eq!(elements, 4);
}
#[test]
fn test_schedule_iterations_modulo() {
let loop_uops: usize = 30;
let issue_width: usize = 4;
let min_ii = (loop_uops + issue_width - 1) / issue_width;
assert_eq!(min_ii, 8);
}
#[test]
fn test_pipeline_resource_balance() {
let port0_uops: usize = 10; let port1_uops: usize = 8; let port5_uops: usize = 6; let total_ports: usize = 3;
let total_uops = port0_uops + port1_uops + port5_uops;
let balanced = total_uops % total_ports == 0; assert!(!balanced); }
#[test]
fn test_combined_unroll_and_jam_pipeline() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_nested_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
for l in &mut optimizer.loops {
l.trip_count = TripCountEstimate::Exact(8);
l.contains_memory_ops = true;
l.body_size = 15;
l.uop_count = 18;
}
let unroll_result = optimizer.run_loop_unrolling();
let jam_result = optimizer.run_unroll_and_jam();
assert!(unroll_result + jam_result >= 0);
}
#[test]
fn test_interchange_followed_by_vectorize() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_nested_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
for l in &mut optimizer.loops {
l.trip_count = TripCountEstimate::Exact(64);
l.contains_memory_ops = true;
l.induction_vars = vec![
InductionVariable::new_basic(1, 0, 1, l.id, 32),
InductionVariable::new_derived(2, 1, 8, 0, l.id, 64),
];
}
optimizer.run_loop_interchange();
assert!(optimizer.stats.interchanged <= optimizer.loops.len());
}
#[test]
fn test_align_plus_nop_sequence() {
let mut r#gen = X86NopGenerator::new(X86MicroArch::IceLake);
let current_offset: u32 = 5;
let alignment: u32 = 32;
let padding = X86NopGenerator::compute_padding(current_offset, alignment);
assert_eq!(padding, 27);
let nops = r#gen.generate_nops(padding);
assert_eq!(nops.len(), 27);
}
#[test]
fn test_align_zero_offset() {
let r#gen = X86NopGenerator::new(X86MicroArch::Skylake);
let padding = X86NopGenerator::compute_padding(0, 16);
assert_eq!(padding, 0);
}
#[test]
fn test_prefetch_streaming_store() {
let calc = PrefetchDistanceCalculator::new(X86MicroArch::Skylake);
let distance = calc.compute_prefetch_distance(64, PrefetchType::NTA);
assert!(distance >= 100); }
#[test]
fn test_prefetch_temporal_reuse() {
let calc = PrefetchDistanceCalculator::new(X86MicroArch::Skylake);
let distance = calc.compute_prefetch_distance(64, PrefetchType::T0);
assert!(distance < 10); }
#[test]
fn test_hybrid_architecture_alder_lake() {
let p_core = X86MicroArch::AlderLakeP;
let e_core = X86MicroArch::AlderLakeE;
assert!(p_core.uop_cache_size() > e_core.uop_cache_size());
assert!(p_core.decode_width() > e_core.decode_width());
assert!(!p_core.has_lsd());
assert!(!e_core.has_lsd());
}
#[test]
fn test_avx512_support_matrix() {
let avx512_capable = [
X86MicroArch::Skylake,
X86MicroArch::IceLake,
X86MicroArch::Zen4,
X86MicroArch::Zen5,
X86MicroArch::GraniteRapids,
];
let non_avx512 = [
X86MicroArch::AlderLakeE,
X86MicroArch::Zen1,
X86MicroArch::Zen2,
X86MicroArch::Zen3,
];
assert!(avx512_capable.len() > 0);
assert!(non_avx512.len() > 0);
for arch in &avx512_capable {
assert!(!arch.has_lsd() || arch.has_lsd()); }
}
#[test]
fn test_rotation_cost_model_applied() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.cost_config = X86LoopCostConfig::aggressive();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
if !optimizer.loops.is_empty() {
let should_rotate = optimizer.should_rotate_loop(0);
assert!(should_rotate || !should_rotate);
}
}
#[test]
fn test_fusion_compatible_exact() {
let mut optimizer = X86LoopOptimizer::new(make_test_subtarget());
let mut a = create_minimal_loop();
a.trip_count = TripCountEstimate::Exact(32);
a.depth = 1;
let mut b = create_minimal_loop();
b.trip_count = TripCountEstimate::Exact(32);
b.depth = 1;
optimizer.loops = vec![a, b];
assert!(optimizer.can_fuse(0, 1));
}
#[test]
fn test_fusion_incompatible_different_depths() {
let mut optimizer = X86LoopOptimizer::new(make_test_subtarget());
let mut a = create_minimal_loop();
a.trip_count = TripCountEstimate::Exact(10);
a.depth = 0;
let mut b = create_minimal_loop();
b.trip_count = TripCountEstimate::Exact(10);
b.depth = 1;
optimizer.loops = vec![a, b];
assert!(!optimizer.can_fuse(0, 1));
}
#[test]
fn test_fusion_incompatible_unknown_count() {
let mut optimizer = X86LoopOptimizer::new(make_test_subtarget());
let mut a = create_minimal_loop();
a.trip_count = TripCountEstimate::Unknown;
a.depth = 0;
let mut b = create_minimal_loop();
b.trip_count = TripCountEstimate::Exact(10);
b.depth = 0;
optimizer.loops = vec![a, b];
assert!(!optimizer.can_fuse(0, 1));
}
#[test]
fn test_distribution_candidate_with_memory() {
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
let loop_info = X86NaturalLoop {
body_size: 12,
contains_memory_ops: true,
contains_calls: false,
..create_minimal_loop()
};
assert!(optimizer.can_distribute(&loop_info));
assert!(optimizer.is_profitable_to_distribute(&loop_info));
}
#[test]
fn test_distribution_candidate_without_memory() {
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
let loop_info = X86NaturalLoop {
body_size: 12,
contains_memory_ops: false,
contains_calls: false,
..create_minimal_loop()
};
assert!(optimizer.can_distribute(&loop_info));
assert!(!optimizer.is_profitable_to_distribute(&loop_info));
}
#[test]
fn test_iv_classification_primary() {
let iv = InductionVariable::new_basic(100, 0, 1, 42, 32);
assert!(iv.is_basic);
assert!(iv.is_integer_iv());
assert_eq!(iv.at_iteration(10), 10);
assert_eq!(iv.at_iteration(100), 100);
}
#[test]
fn test_iv_classification_secondary() {
let iv = InductionVariable::new_derived(200, 100, 4, 16, 42, 64);
assert!(!iv.is_basic);
assert_eq!(iv.at_iteration(0), 16);
assert_eq!(iv.at_iteration(1), 20);
assert_eq!(iv.at_iteration(5), 36);
}
#[test]
fn test_iv_negative_step() {
let iv = InductionVariable::new_basic(300, 100, -1, 0, 32);
assert_eq!(iv.at_iteration(0), 100);
assert_eq!(iv.at_iteration(10), 90);
assert_eq!(iv.at_iteration(50), 50);
}
#[test]
fn test_iv_zero_step() {
let iv = InductionVariable::new_basic(400, 42, 0, 0, 32);
assert_eq!(iv.at_iteration(0), 42);
assert_eq!(iv.at_iteration(10), 42);
assert_eq!(iv.at_iteration(1000), 42);
}
#[test]
fn test_nest_depth_triple() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let outer = X86NaturalLoop {
id: 100,
header: 1,
blocks: vec![1, 2, 3, 4, 5, 6],
parent: None,
children: vec![200],
depth: 0,
..create_minimal_loop()
};
let middle = X86NaturalLoop {
id: 200,
header: 2,
blocks: vec![2, 3, 4, 5],
parent: Some(100),
children: vec![300],
depth: 1,
..create_minimal_loop()
};
let inner = X86NaturalLoop {
id: 300,
header: 3,
blocks: vec![3, 4],
parent: Some(200),
children: vec![],
depth: 2,
..create_minimal_loop()
};
optimizer.loops = vec![outer, middle, inner];
optimizer.loops[0].trip_count = TripCountEstimate::Exact(10);
optimizer.loops[1].trip_count = TripCountEstimate::Exact(10);
optimizer.loops[2].trip_count = TripCountEstimate::Exact(10);
let freq = LoopAnalysisUtil::nest_frequency(&optimizer.loops[2], &optimizer.loops);
assert_eq!(freq, 1000.0);
}
#[test]
fn test_cost_config_compute_unroll_factor_one() {
let config = X86LoopCostConfig {
uop_cache_capacity: 2,
max_unroll_factor: 8,
..X86LoopCostConfig::default()
};
let tc = TripCountEstimate::Exact(32);
let factor = config.compute_unroll_factor(&tc, 10);
assert_eq!(factor, 1);
}
#[test]
fn test_cost_config_compute_unroll_factor_even_division() {
let config = X86LoopCostConfig::default();
let tc = TripCountEstimate::Exact(16);
let factor = config.compute_unroll_factor(&tc, 10);
assert!(factor >= 2);
}
#[test]
fn test_cost_config_compute_unroll_factor_prime() {
let config = X86LoopCostConfig::default();
let tc = TripCountEstimate::Exact(17);
let factor = config.compute_unroll_factor(&tc, 10);
assert!(factor >= 1);
}
#[test]
fn test_lsd_capacity_truncates_unroll_factor() {
let mut config = X86LoopCostConfig::for_microarch(X86MicroArch::Skylake);
config.lsd_capacity = 28;
let tc = TripCountEstimate::Exact(16);
let factor = config.compute_unroll_factor(&tc, 10);
assert!(factor <= 2); }
#[test]
fn test_trip_count_exact_zero() {
let tc = TripCountEstimate::Exact(0);
assert_eq!(tc.as_exact(), Some(0));
assert_eq!(tc.as_bound(), Some(0));
assert!(tc.is_exact());
}
#[test]
fn test_trip_count_exact_large() {
let tc = TripCountEstimate::Exact(u64::MAX);
assert_eq!(tc.as_bound(), Some(u64::MAX));
}
#[test]
fn test_trip_count_max_large() {
let tc = TripCountEstimate::Max(u64::MAX);
assert_eq!(tc.as_exact(), None);
assert!(tc.has_bound());
}
#[test]
fn test_scev_trip_count_negative_step() {
let scev = X86SCEV::new(X86MicroArch::Skylake);
let add_rec = SCEV::AddRec {
base: Box::new(SCEV::Constant(100)),
step: Box::new(SCEV::Constant(-1)),
loop_header: 1,
is_signed: true,
};
let bound = SCEV::Constant(0);
let tc = scev.trip_count_from_exit(&add_rec, &bound, ICmpPred::Sgt);
}
#[test]
fn test_scev_trip_count_non_constant_bound() {
let scev = X86SCEV::new(X86MicroArch::Skylake);
let add_rec = SCEV::AddRec {
base: Box::new(SCEV::Constant(0)),
step: Box::new(SCEV::Constant(1)),
loop_header: 1,
is_signed: false,
};
let bound = SCEV::Unknown;
let tc = scev.trip_count_from_exit(&add_rec, &bound, ICmpPred::Ult);
assert_eq!(tc, None);
}
#[test]
fn test_scev_decompose_non_addrec() {
let scev = X86SCEV::new(X86MicroArch::Skylake);
assert!(scev.decompose_add_rec(&SCEV::Constant(42)).is_none());
assert!(scev.decompose_add_rec(&SCEV::Unknown).is_none());
}
#[test]
fn test_lsd_all_microarchs() {
let archs_with_lsd = [
X86MicroArch::SandyBridge,
X86MicroArch::Haswell,
X86MicroArch::Skylake,
X86MicroArch::IceLake,
];
for arch in &archs_with_lsd {
let lsd = LSDOptimizer::new(*arch);
assert!(lsd.available, "{:?} should have LSD", arch);
}
let archs_without_lsd = [
X86MicroArch::AlderLakeP,
X86MicroArch::Zen4,
X86MicroArch::Generic,
];
for arch in &archs_without_lsd {
let lsd = LSDOptimizer::new(*arch);
assert!(!lsd.available, "{:?} should not have LSD", arch);
}
}
#[test]
fn test_lsd_boundary_exact_fit() {
let lsd = LSDOptimizer::new(X86MicroArch::Haswell);
assert!(lsd.loop_fits_in_lsd(56, 128, false, false));
assert!(!lsd.loop_fits_in_lsd(57, 128, false, false));
}
#[test]
fn test_zen_buffer_all_zen() {
let zen_archs = [
X86MicroArch::Zen1,
X86MicroArch::Zen2,
X86MicroArch::Zen3,
X86MicroArch::Zen4,
X86MicroArch::Zen5,
];
for arch in &zen_archs {
let buf = ZenLoopBuffer::new(*arch);
assert!(buf.available, "{:?} should have op cache", arch);
assert!(buf.capacity > 0, "{:?} should have positive capacity", arch);
}
}
#[test]
fn test_zen_buffer_exact_boundary() {
let buf = ZenLoopBuffer::new(X86MicroArch::Zen3);
assert!(buf.loop_fits_in_op_cache(4096));
assert!(!buf.loop_fits_in_op_cache(4097));
}
#[test]
fn test_prefetch_calculator_all_microarchs() {
let archs = [
X86MicroArch::Skylake,
X86MicroArch::IceLake,
X86MicroArch::AlderLakeP,
X86MicroArch::Zen3,
X86MicroArch::Zen4,
X86MicroArch::Zen5,
];
for arch in &archs {
let calc = PrefetchDistanceCalculator::new(*arch);
assert!(calc.cache_line_size == 64);
assert!(calc.l1_latency > 0);
assert!(calc.l2_latency > 0);
}
}
#[test]
fn test_prefetch_distance_clamping() {
let calc = PrefetchDistanceCalculator::default();
let distance = calc.compute_prefetch_distance(65536, PrefetchType::NTA);
assert!(distance <= 1024);
}
#[test]
fn test_prefetch_beneficial_boundary() {
let calc = PrefetchDistanceCalculator::default();
let tc = TripCountEstimate::Exact(8);
assert!(calc.is_prefetch_beneficial(&tc, 16));
assert!(!calc.is_prefetch_beneficial(&tc, 8));
}
#[test]
fn test_nop_generator_all_lengths() {
let mut r#gen = X86NopGenerator::new(X86MicroArch::Skylake);
for len in 1..=64 {
let nops = r#gen.generate_nops(len);
assert_eq!(nops.len(), len as usize, "Failed for length {}", len);
}
}
#[test]
fn test_nop_generator_total_tracking() {
let mut r#gen = X86NopGenerator::new(X86MicroArch::Haswell);
let _ = r#gen.generate_nops(5);
let _ = r#gen.generate_nops(10);
let _ = r#gen.generate_nops(3);
assert_eq!(r#gen.total_bytes_padded, 18);
}
#[test]
fn test_btb_no_alias_different_indices() {
let btb = X86BTBOptimizer::new(X86MicroArch::Skylake);
assert!(!btb.has_btb_alias(0x1000, 0x2000));
}
#[test]
fn test_btb_recommend_no_alias() {
let btb = X86BTBOptimizer::new(X86MicroArch::Skylake);
let offset = btb.recommend_offset(0x1000, 0x2000);
assert_eq!(offset, 0);
}
#[test]
fn test_is_counting_loop_no_ivs() {
let loop_info = create_minimal_loop();
assert!(!LoopAnalysisUtil::is_counting_loop(&loop_info));
}
#[test]
fn test_is_counting_loop_only_derived() {
let loop_info = X86NaturalLoop {
induction_vars: vec![InductionVariable::new_derived(1, 0, 1, 0, 0, 32)],
..create_minimal_loop()
};
assert!(!LoopAnalysisUtil::is_counting_loop(&loop_info));
}
#[test]
fn test_is_hot_loop_symbolic() {
assert!(LoopAnalysisUtil::is_hot_loop(
&TripCountEstimate::Symbolic("N".to_string()),
10
));
}
#[test]
fn test_is_hot_loop_large_body() {
assert!(LoopAnalysisUtil::is_hot_loop(
&TripCountEstimate::Unknown,
100
));
}
#[test]
fn test_nest_frequency_max_bound() {
let mut outer = create_minimal_loop();
outer.trip_count = TripCountEstimate::Max(20);
let freq = LoopAnalysisUtil::nest_frequency(&outer, &[]);
assert_eq!(freq, 20.0);
}
#[test]
fn test_dependence_all_directions() {
let directions = vec![
DependenceDirection::Negative,
DependenceDirection::Zero,
DependenceDirection::Positive,
DependenceDirection::Unknown,
];
assert_eq!(directions.len(), 4);
}
#[test]
fn test_dependence_all_types() {
let types = vec![
DependenceType::Flow,
DependenceType::Anti,
DependenceType::Output,
DependenceType::Input,
];
assert_eq!(types.len(), 4);
}
#[test]
fn test_dependence_cannot_interchange_with_negative() {
let mut analyzer = LoopDependenceAnalyzer::new();
analyzer.distance_vectors.push(DependenceVector {
source: 1,
target: 2,
directions: vec![DependenceDirection::Negative],
distances: vec![Some(-1)],
dep_type: DependenceType::Anti,
});
assert!(!analyzer.can_interchange());
}
#[test]
fn test_loop_cost_all_fields() {
let config = X86LoopCostConfig::default();
let loop_info = X86NaturalLoop {
uop_count: 30,
body_size: 25,
blocks: vec![1, 2, 3],
contains_memory_ops: true,
..create_minimal_loop()
};
let cost = LoopCost::estimate(&loop_info, &config);
assert_eq!(cost.branches, 3);
assert!(cost.memory_ops > 0);
assert!(cost.cycles_per_iter > 0.0);
assert!(cost.total_cost(100) > 0.0);
}
#[test]
fn test_loop_cost_fits_in_lsd() {
let mut config = X86LoopCostConfig::default();
config.lsd_available = true;
config.lsd_capacity = 30;
let loop_info = X86NaturalLoop {
uop_count: 20,
..create_minimal_loop()
};
let cost = LoopCost::estimate(&loop_info, &config);
assert!(cost.fits_in_lsd);
}
#[test]
fn test_loop_cost_no_lsd() {
let mut config = X86LoopCostConfig::default();
config.lsd_available = false;
let loop_info = X86NaturalLoop {
uop_count: 10,
..create_minimal_loop()
};
let cost = LoopCost::estimate(&loop_info, &config);
assert!(!cost.fits_in_lsd);
}
#[test]
fn test_pass_manager_stats() {
let subtarget = make_test_subtarget();
let pass = X86LoopOptimizerPass::new(subtarget);
let stats = pass.stats();
assert!(!stats.made_progress());
}
#[test]
fn test_pass_manager_priority() {
let subtarget = make_test_subtarget();
let pass = X86LoopOptimizerPass::new(subtarget);
assert_eq!(pass.priority, 50);
}
#[test]
fn test_stats_merge_preserves_other_fields() {
let mut a = X86LoopOptStats::new();
a.loops_rotated = 1;
a.fully_unrolled = 2;
let mut b = X86LoopOptStats::new();
b.prefetches_inserted = 5;
b.headers_aligned = 3;
a.merge(&b);
assert_eq!(a.loops_rotated, 1);
assert_eq!(a.fully_unrolled, 2);
assert_eq!(a.prefetches_inserted, 5);
assert_eq!(a.headers_aligned, 3);
}
#[test]
fn test_stats_made_progress_all_fields() {
let fields: Vec<Box<dyn Fn(&mut X86LoopOptStats)>> = vec![
Box::new(|s: &mut X86LoopOptStats| s.loops_rotated = 1),
Box::new(|s: &mut X86LoopOptStats| s.fully_unrolled = 1),
Box::new(|s: &mut X86LoopOptStats| s.partially_unrolled = 1),
Box::new(|s: &mut X86LoopOptStats| s.unroll_and_jammed = 1),
Box::new(|s: &mut X86LoopOptStats| s.fused = 1),
Box::new(|s: &mut X86LoopOptStats| s.distributed = 1),
Box::new(|s: &mut X86LoopOptStats| s.interchanged = 1),
Box::new(|s: &mut X86LoopOptStats| s.unswitched = 1),
Box::new(|s: &mut X86LoopOptStats| s.idioms_recognized = 1),
Box::new(|s: &mut X86LoopOptStats| s.deleted = 1),
Box::new(|s: &mut X86LoopOptStats| s.simplified = 1),
Box::new(|s: &mut X86LoopOptStats| s.strength_reduced = 1),
Box::new(|s: &mut X86LoopOptStats| s.rerolled = 1),
Box::new(|s: &mut X86LoopOptStats| s.versioned = 1),
Box::new(|s: &mut X86LoopOptStats| s.predicated = 1),
Box::new(|s: &mut X86LoopOptStats| s.ivs_optimized = 1),
];
for setter in fields {
let mut stats = X86LoopOptStats::new();
setter(&mut stats);
assert!(stats.made_progress(), "Field should trigger made_progress");
}
}
#[test]
fn test_reset_clears_all_state() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
assert!(!optimizer.loops.is_empty());
optimizer.reset();
assert!(optimizer.loops.is_empty());
assert_eq!(optimizer.loops_analyzed, 0);
assert!(!optimizer.stats.made_progress());
}
#[test]
fn test_run_pipeline_twice() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
let stats1 = optimizer
.run_pipeline(&func, &blocks, &pred_map, &succ_map)
.clone();
let stats2 = optimizer
.run_pipeline(&func, &blocks, &pred_map, &succ_map)
.clone();
assert_eq!(optimizer.loops_analyzed, optimizer.loops_analyzed);
}
#[test]
fn test_empty_blocks_produce_no_loops() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let func = ValueRef::new_function("empty");
let empty_blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
let empty_preds: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
let empty_succs: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
optimizer.detect_loops(&func, &empty_blocks, &empty_preds, &empty_succs);
assert!(optimizer.loops.is_empty());
}
#[test]
fn test_single_block_no_loop() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let func = ValueRef::new_function("single");
let mut blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
blocks.insert(1, vec![]);
let mut pred_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
pred_map.insert(1, vec![]);
let mut succ_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
succ_map.insert(1, vec![]);
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
assert_eq!(optimizer.loops.len(), 0);
}
#[test]
fn test_body_size_estimation() {
let mut blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
blocks.insert(1, vec![1, 2, 3]);
blocks.insert(2, vec![4, 5]);
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
let size = optimizer.estimate_body_size(&[1, 2], &blocks);
assert_eq!(size, 5);
}
#[test]
fn test_uop_estimation_ratio() {
let mut blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
blocks.insert(1, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
let body_size = optimizer.estimate_body_size(&[1], &blocks);
let uop_count = optimizer.estimate_uop_count(&[1], &blocks);
assert_eq!(body_size, 10);
assert_eq!(uop_count, 12); }
#[test]
fn test_detect_call_in_loop() {
let mut blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
let call = 100u64;
blocks.insert(1, vec![1, call, 2]);
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
assert!(optimizer.loop_contains_calls(&[1], &blocks));
}
#[test]
fn test_detect_memory_in_loop() {
let mut blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
let load = 200u64;
blocks.insert(1, vec![load]);
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
assert!(optimizer.loop_contains_memory_ops(&[1], &blocks));
}
#[test]
fn test_versioning_cannot_without_memory() {
let loop_info = X86NaturalLoop {
contains_memory_ops: false,
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.can_version_for_alias(&loop_info));
}
#[test]
fn test_versioning_cannot_without_ivs() {
let loop_info = X86NaturalLoop {
contains_memory_ops: true,
induction_vars: vec![],
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.can_version_for_alias(&loop_info));
}
#[test]
fn test_alignment_versioning_needs_iv() {
let loop_info = X86NaturalLoop {
contains_memory_ops: true,
induction_vars: vec![],
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.can_version_for_alignment(&loop_info));
}
#[test]
fn test_unswitch_profitable_max_high() {
let loop_info = X86NaturalLoop {
trip_count: TripCountEstimate::Max(32),
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(optimizer.is_profitable_to_unswitch(&loop_info));
}
#[test]
fn test_unswitch_not_profitable_max_low() {
let loop_info = X86NaturalLoop {
trip_count: TripCountEstimate::Max(8),
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.is_profitable_to_unswitch(&loop_info));
}
#[test]
fn test_reroll_candidate_exact_boundary() {
let loop_info = X86NaturalLoop {
body_size: 21,
trip_count: TripCountEstimate::Exact(4),
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(optimizer.is_candidate_for_reroll(&loop_info));
}
#[test]
fn test_reroll_not_candidate_large_trip() {
let loop_info = X86NaturalLoop {
body_size: 50,
trip_count: TripCountEstimate::Exact(10),
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.is_candidate_for_reroll(&loop_info));
}
#[test]
fn test_dominator_tree_linear() {
let mut blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
blocks.insert(1, vec![]);
blocks.insert(2, vec![]);
blocks.insert(3, vec![]);
let mut pred_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
pred_map.insert(1, vec![]);
pred_map.insert(2, vec![1]);
pred_map.insert(3, vec![2]);
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
let dom_tree = optimizer.build_dominator_tree(&blocks, &pred_map);
assert_eq!(dom_tree.idoms.get(&2), Some(&1));
assert_eq!(dom_tree.idoms.get(&3), Some(&2));
}
#[test]
fn test_dominator_tree_diamond() {
let mut blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
blocks.insert(1, vec![]);
blocks.insert(2, vec![]);
blocks.insert(3, vec![]);
blocks.insert(4, vec![]);
let mut pred_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
pred_map.insert(1, vec![]);
pred_map.insert(2, vec![1]);
pred_map.insert(3, vec![1]);
pred_map.insert(4, vec![2, 3]);
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
let dom_tree = optimizer.build_dominator_tree(&blocks, &pred_map);
assert_eq!(dom_tree.idoms.get(&2), Some(&1));
assert_eq!(dom_tree.idoms.get(&3), Some(&1));
assert_eq!(dom_tree.idoms.get(&4), Some(&1));
}
#[test]
fn test_dominates_self() {
let mut blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
blocks.insert(1, vec![]);
let mut pred_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
pred_map.insert(1, vec![]);
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
let dom_tree = optimizer.build_dominator_tree(&blocks, &pred_map);
assert!(optimizer.dominates(1, 1, &dom_tree));
}
#[test]
fn test_find_preheader_unique() {
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
let mut pred_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
pred_map.insert(2, vec![1, 3]);
let preheader = optimizer.find_preheader(2, &[2, 3], &pred_map);
assert_eq!(preheader, Some(1));
}
#[test]
fn test_find_preheader_multiple_entries() {
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
let mut pred_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
pred_map.insert(2, vec![1, 4, 3]);
let preheader = optimizer.find_preheader(2, &[2, 3], &pred_map);
assert_eq!(preheader, None);
}
#[test]
fn test_find_preheader_all_inside() {
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
let mut pred_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
pred_map.insert(2, vec![3, 4]);
let preheader = optimizer.find_preheader(2, &[2, 3, 4], &pred_map);
assert_eq!(preheader, None);
}
#[test]
fn test_loop_reducible_standard() {
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
let mut pred_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
pred_map.insert(1, vec![]); pred_map.insert(2, vec![1, 3]); pred_map.insert(3, vec![2]);
assert!(optimizer.is_loop_reducible(2, &[2, 3], &pred_map));
}
#[test]
fn test_loop_irreducible_multiple_entries() {
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
let mut pred_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
pred_map.insert(1, vec![]);
pred_map.insert(2, vec![1, 3]); pred_map.insert(3, vec![2]);
pred_map.insert(4, vec![1, 3]);
assert!(!optimizer.is_loop_reducible(2, &[2, 3], &pred_map));
}
#[test]
fn test_block_to_loop_mapping() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
for loop_info in &optimizer.loops {
for block in &loop_info.blocks {
assert!(optimizer.block_to_loop.contains_key(block));
}
}
}
#[test]
fn test_innermost_loop_returns_deepest() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let outer = X86NaturalLoop {
id: 10,
header: 1,
blocks: vec![1, 2, 3, 4, 5],
children: vec![20],
..create_minimal_loop()
};
let inner = X86NaturalLoop {
id: 20,
header: 3,
blocks: vec![3, 4],
parent: Some(10),
..create_minimal_loop()
};
optimizer.loops = vec![outer, inner];
optimizer.block_to_loop.insert(1, 10);
optimizer.block_to_loop.insert(2, 10);
optimizer.block_to_loop.insert(3, 20);
optimizer.block_to_loop.insert(4, 20);
optimizer.block_to_loop.insert(5, 10);
let result = optimizer.get_innermost_loop_for_block(3);
assert!(result.is_some());
assert_eq!(result.unwrap().id, 20);
}
#[test]
fn test_find_back_edges_detects_loop() {
let subtarget = make_test_subtarget();
let optimizer = X86LoopOptimizer::new(subtarget);
let mut blocks: HashMap<BlockId, Vec<ValueId>> = HashMap::new();
blocks.insert(1, vec![]);
blocks.insert(2, vec![]);
let mut succ_map: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
succ_map.insert(1, vec![2]);
succ_map.insert(2, vec![1]);
let dom_tree = optimizer.build_dominator_tree(&blocks, &HashMap::new());
let backedges = optimizer.find_back_edges(&blocks, &succ_map, &dom_tree);
let _ = backedges;
}
#[test]
fn test_memcpy_recognition_multiple_ivs() {
let loop_info = X86NaturalLoop {
blocks: vec![1],
contains_memory_ops: true,
induction_vars: vec![
InductionVariable::new_basic(1, 0, 1, 0, 64),
InductionVariable::new_basic(2, 0, 1, 0, 64),
],
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(optimizer.try_recognize_memcpy(&loop_info));
}
#[test]
fn test_popcount_recognition_small_trip() {
let loop_info = X86NaturalLoop {
blocks: vec![1],
contains_memory_ops: false,
trip_count: TripCountEstimate::Exact(32),
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(optimizer.try_recognize_popcount(&loop_info));
}
#[test]
fn test_popcount_not_recognized_large() {
let loop_info = X86NaturalLoop {
blocks: vec![1],
contains_memory_ops: false,
trip_count: TripCountEstimate::Exact(65),
..create_minimal_loop()
};
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.try_recognize_popcount(&loop_info));
}
#[test]
fn test_strlen_not_recognized() {
let loop_info = create_minimal_loop();
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.try_recognize_strlen(&loop_info));
}
#[test]
fn test_memcmp_not_recognized() {
let loop_info = create_minimal_loop();
let optimizer = X86LoopOptimizer::new(make_test_subtarget());
assert!(!optimizer.try_recognize_memcmp(&loop_info));
}
#[test]
fn test_simplify_already_canonical() {
let loop_info = X86NaturalLoop {
latches: vec![2],
preheader: Some(0),
is_canonical: false,
..create_minimal_loop()
};
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
optimizer.loops = vec![loop_info];
let simplified = optimizer.run_loop_simplify();
assert!(simplified == 0 || simplified == 1);
}
#[test]
fn test_icelake_decode_width_affects_throughput() {
let microarch = X86MicroArch::IceLake;
assert_eq!(microarch.decode_width(), 5);
}
#[test]
fn test_alder_lake_e_core_constraints() {
let microarch = X86MicroArch::AlderLakeE;
assert_eq!(microarch.decode_width(), 3);
assert_eq!(microarch.uop_cache_size(), 2048);
assert!(!microarch.has_lsd());
}
#[test]
fn test_strength_reduction_no_ivs() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
if !optimizer.loops.is_empty() {
optimizer.loops[0].body_size = 10;
}
let reduced = optimizer.run_strength_reduction();
assert_eq!(reduced, 0);
}
#[test]
fn test_transformation_order_preserves_analysis() {
let subtarget = make_test_subtarget();
let mut optimizer = X86LoopOptimizer::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_nested_loop_cfg();
optimizer.detect_loops(&func, &blocks, &pred_map, &succ_map);
let initial_count = optimizer.loops.len();
optimizer.run_loop_simplify();
optimizer.run_loop_rotation();
optimizer.run_loop_interchange();
optimizer.run_loop_unswitching();
assert_eq!(optimizer.loops.len(), initial_count);
}
#[test]
fn test_microarch_enum_all_variants() {
let all = vec![
X86MicroArch::Core2,
X86MicroArch::Nehalem,
X86MicroArch::SandyBridge,
X86MicroArch::Haswell,
X86MicroArch::Skylake,
X86MicroArch::IceLake,
X86MicroArch::AlderLakeP,
X86MicroArch::AlderLakeE,
X86MicroArch::GraniteRapids,
X86MicroArch::K8,
X86MicroArch::Bulldozer,
X86MicroArch::Zen1,
X86MicroArch::Zen2,
X86MicroArch::Zen3,
X86MicroArch::Zen4,
X86MicroArch::Zen5,
X86MicroArch::Generic,
];
for arch in &all {
let _ = format!("{:?}", arch);
let _ = arch.has_lsd();
let _ = arch.has_uop_cache();
let _ = arch.lsd_issue_width();
let _ = arch.preferred_loop_alignment();
let _ = X86LoopCostConfig::for_microarch(*arch);
let _ = LoopAlignmentPolicy::for_microarch(*arch);
let _ = PrefetchDistanceCalculator::new(*arch);
let _ = LSDOptimizer::new(*arch);
let _ = ZenLoopBuffer::new(*arch);
let _ = X86BTBOptimizer::new(*arch);
}
}
#[test]
fn test_peeler_creation() {
let peeler = X86LoopPeeler::new();
assert_eq!(peeler.max_peel_count, 16);
assert!(peeler.peel_for_alignment);
}
#[test]
fn test_peeler_compute_no_peel() {
let peeler = X86LoopPeeler::new();
let tc = TripCountEstimate::Exact(32);
let count = peeler.compute_peel_count(&tc, 8, 0, 4);
assert_eq!(count, 0);
}
#[test]
fn test_peeler_compute_remainder() {
let peeler = X86LoopPeeler::new();
let tc = TripCountEstimate::Exact(35);
let count = peeler.compute_peel_count(&tc, 8, 0, 4);
assert_eq!(count, 3);
}
#[test]
fn test_peeler_compute_alignment() {
let peeler = X86LoopPeeler::new();
let tc = TripCountEstimate::Exact(100);
let count = peeler.compute_peel_count(&tc, 8, 12, 4);
assert_eq!(count, 13);
}
#[test]
fn test_peeler_capped_by_max() {
let mut peeler = X86LoopPeeler::new();
peeler.max_peel_count = 5;
let tc = TripCountEstimate::Exact(100);
let count = peeler.compute_peel_count(&tc, 8, 12, 4);
assert!(count <= 5);
}
#[test]
fn test_peeling_beneficial_large_trip() {
let peeler = X86LoopPeeler::new();
assert!(peeler.is_peeling_beneficial(&TripCountEstimate::Exact(16), 5));
}
#[test]
fn test_peeling_not_beneficial_small() {
let peeler = X86LoopPeeler::new();
assert!(!peeler.is_peeling_beneficial(&TripCountEstimate::Exact(3), 2));
}
#[test]
fn test_tiler_creation() {
let tiler = X86LoopTiler::new(X86MicroArch::Skylake);
assert_eq!(tiler.l1_cache_size, 32768);
assert!(tiler.max_tile_dim > 0);
}
#[test]
fn test_tiler_compute_tile_size_l1() {
let tiler = X86LoopTiler::new(X86MicroArch::Skylake);
let tile = tiler.compute_tile_size(8, 3, false);
assert!(tile >= 8);
assert!(tile <= 512);
}
#[test]
fn test_tiler_compute_tile_size_l2() {
let tiler = X86LoopTiler::new(X86MicroArch::Skylake);
let tile = tiler.compute_tile_size(4, 2, true);
assert!(tile >= tiler.min_tile_dim);
assert!(tile <= tiler.max_tile_dim);
}
#[test]
fn test_tiler_tile_bounds() {
let tiler = X86LoopTiler::new(X86MicroArch::Skylake);
let bounds = tiler.tile_bounds(100, 32);
assert_eq!(bounds.len(), 4);
assert_eq!(bounds[0], (0, 32));
assert_eq!(bounds[3], (96, 100));
}
#[test]
fn test_tiler_applicable_large_trips() {
let tiler = X86LoopTiler::new(X86MicroArch::Skylake);
let outer = X86NaturalLoop {
trip_count: TripCountEstimate::Exact(64),
..create_minimal_loop()
};
let inner = X86NaturalLoop {
trip_count: TripCountEstimate::Exact(128),
..create_minimal_loop()
};
assert!(tiler.is_tiling_applicable(&outer, &inner));
}
#[test]
fn test_tiler_not_applicable_small_trips() {
let tiler = X86LoopTiler::new(X86MicroArch::Skylake);
let outer = X86NaturalLoop {
trip_count: TripCountEstimate::Exact(4),
..create_minimal_loop()
};
let inner = X86NaturalLoop {
trip_count: TripCountEstimate::Exact(8),
..create_minimal_loop()
};
assert!(!tiler.is_tiling_applicable(&outer, &inner));
}
#[test]
fn test_guard_analyzer_dead_loop() {
let mut guard_opt = X86LoopGuardOptimizer::new();
let loop_info = X86NaturalLoop {
preheader: Some(0),
trip_count: TripCountEstimate::Exact(0),
..create_minimal_loop()
};
let result = guard_opt.analyze_guard(&loop_info);
assert_eq!(result, Some(GuardAnalysisResult::DeadLoop));
}
#[test]
fn test_guard_analyzer_single_iter() {
let mut guard_opt = X86LoopGuardOptimizer::new();
let loop_info = X86NaturalLoop {
preheader: Some(0),
trip_count: TripCountEstimate::Exact(1),
..create_minimal_loop()
};
let result = guard_opt.analyze_guard(&loop_info);
assert_eq!(result, Some(GuardAnalysisResult::SingleIteration));
}
#[test]
fn test_guard_widen() {
let mut guard_opt = X86LoopGuardOptimizer::new();
let loop_info = create_minimal_loop();
assert!(guard_opt.widen_guard(&loop_info, 4));
assert_eq!(guard_opt.guards_widened, 1);
}
#[test]
fn test_guard_widen_noop_min_trip_one() {
let mut guard_opt = X86LoopGuardOptimizer::new();
let loop_info = create_minimal_loop();
assert!(!guard_opt.widen_guard(&loop_info, 1));
}
#[test]
fn test_guard_eliminate() {
let mut guard_opt = X86LoopGuardOptimizer::new();
guard_opt.eliminate_guard();
assert_eq!(guard_opt.guards_eliminated, 1);
}
#[test]
fn test_loop_counter_pattern_skylake() {
let patterns = X86LoopCodegenPatterns::new(X86MicroArch::Skylake);
assert_eq!(
patterns.loop_counter_pattern(),
LoopCounterPattern::DecJccFused
);
}
#[test]
fn test_loop_counter_pattern_nehalem() {
let patterns = X86LoopCodegenPatterns::new(X86MicroArch::Nehalem);
assert_eq!(patterns.loop_counter_pattern(), LoopCounterPattern::DecJcc);
}
#[test]
fn test_address_update_pattern_zen() {
let patterns = X86LoopCodegenPatterns::new(X86MicroArch::Zen4);
assert_eq!(
patterns.address_update_pattern(),
AddressUpdatePattern::LeaAgu
);
}
#[test]
fn test_simd_profitable_avx512() {
let patterns = X86LoopCodegenPatterns::new(X86MicroArch::IceLake);
assert!(patterns.simd_profitable(&TripCountEstimate::Exact(32), 4));
}
#[test]
fn test_simd_not_profitable_small_trip() {
let patterns = X86LoopCodegenPatterns::new(X86MicroArch::Skylake);
assert!(!patterns.simd_profitable(&TripCountEstimate::Exact(4), 4));
}
#[test]
fn test_scheduler_skylake() {
let sched = X86LoopScheduler::new(X86MicroArch::Skylake);
assert_eq!(sched.num_ports, 8);
assert!(sched.port6_branch);
}
#[test]
fn test_scheduler_zen4() {
let sched = X86LoopScheduler::new(X86MicroArch::Zen4);
assert_eq!(sched.num_ports, 10);
assert!(!sched.port6_branch);
}
#[test]
fn test_scheduler_estimate_cycles() {
let sched = X86LoopScheduler::new(X86MicroArch::Skylake);
let cycles = sched.estimate_cycles(10, 4, 1);
assert!(cycles > 0.0);
}
#[test]
fn test_scheduler_port_balanced() {
let sched = X86LoopScheduler::new(X86MicroArch::Skylake);
assert!(sched.is_port_balanced(&[5, 5, 5, 5]));
}
#[test]
fn test_scheduler_port_unbalanced() {
let sched = X86LoopScheduler::new(X86MicroArch::Skylake);
assert!(!sched.is_port_balanced(&[10, 1, 1, 1]));
}
#[test]
fn test_buffer_analysis_skylake_lsd_fit() {
let buf = X86LoopBufferAnalysis::new(X86MicroArch::Skylake);
assert_eq!(buf.classify_fit(20), LoopBufferFit::FitsInLSD);
}
#[test]
fn test_buffer_analysis_skylake_dsb_fit() {
let buf = X86LoopBufferAnalysis::new(X86MicroArch::Skylake);
assert_eq!(buf.classify_fit(200), LoopBufferFit::FitsInDSB);
}
#[test]
fn test_buffer_analysis_exceeds_all() {
let buf = X86LoopBufferAnalysis::new(X86MicroArch::Skylake);
assert_eq!(buf.classify_fit(2000), LoopBufferFit::ExceedsAllBuffers);
}
#[test]
fn test_buffer_recommendations() {
let buf = X86LoopBufferAnalysis::new(X86MicroArch::Haswell);
let recs = buf.buffer_recommendations(2000);
assert!(!recs.is_empty());
}
#[test]
fn test_exit_profiler_single_exit() {
let mut profiler = LoopExitProfiler::new();
let loop_info = X86NaturalLoop {
exit_edges: vec![(1, 2)],
..create_minimal_loop()
};
profiler.analyze_exits(&loop_info);
assert_eq!(profiler.hot_exit, Some(2));
assert!((profiler.hot_exit_probability - 1.0).abs() < 0.001);
}
#[test]
fn test_exit_profiler_multiple_exits() {
let mut profiler = LoopExitProfiler::new();
let loop_info = X86NaturalLoop {
exit_edges: vec![(1, 2), (3, 4), (5, 6)],
..create_minimal_loop()
};
profiler.analyze_exits(&loop_info);
assert_eq!(profiler.hot_exit, Some(2));
assert_eq!(profiler.cold_exits.len(), 2);
}
#[test]
fn test_exit_profiler_no_exits() {
let mut profiler = LoopExitProfiler::new();
let loop_info = create_minimal_loop();
profiler.analyze_exits(&loop_info);
assert_eq!(profiler.hot_exit, None);
}
#[test]
fn test_exit_branch_layout_hot() {
let mut profiler = LoopExitProfiler::new();
let loop_info = X86NaturalLoop {
exit_edges: vec![(1, 2)],
..create_minimal_loop()
};
profiler.analyze_exits(&loop_info);
assert_eq!(profiler.exit_branch_layout(1, 2), BranchLayout::FallThrough);
}
#[test]
fn test_exit_branch_layout_cold() {
let mut profiler = LoopExitProfiler::new();
let loop_info = X86NaturalLoop {
exit_edges: vec![(1, 2), (3, 4)],
..create_minimal_loop()
};
profiler.analyze_exits(&loop_info);
assert_eq!(profiler.exit_branch_layout(3, 4), BranchLayout::JumpCold);
}
#[test]
fn test_hint_unroll_pragma() {
let mut handler = X86LoopHintHandler::new();
handler.parse_unroll_pragma(42, 4);
assert_eq!(handler.get_unroll_hint(42), Some(4));
}
#[test]
fn test_hint_nounroll_pragma() {
let mut handler = X86LoopHintHandler::new();
handler.parse_unroll_pragma(10, 0);
assert_eq!(handler.get_unroll_hint(10), Some(0));
assert!(handler.pragma_nounroll.contains(&10));
}
#[test]
fn test_hint_full_unroll_pragma() {
let mut handler = X86LoopHintHandler::new();
handler.parse_unroll_pragma(5, 1);
assert_eq!(handler.get_unroll_hint(5), Some(1));
assert!(handler.pragma_full_unroll.contains(&5));
}
#[test]
fn test_hint_vectorize_pragma() {
let mut handler = X86LoopHintHandler::new();
handler.parse_vectorize_pragma(99, true);
assert_eq!(handler.is_vectorize_enabled(99), Some(true));
}
#[test]
fn test_hint_pgo_hot() {
let handler = X86LoopHintHandler::new();
assert!(handler.is_hot_from_pgo(950.0, 1000.0));
}
#[test]
fn test_hint_pgo_cold() {
let handler = X86LoopHintHandler::new();
assert!(handler.is_cold_from_pgo(50.0, 1000.0));
}
#[test]
fn test_hint_pgo_not_hot() {
let handler = X86LoopHintHandler::new();
assert!(!handler.is_hot_from_pgo(500.0, 1000.0));
}
#[test]
fn test_cache_estimator_skylake() {
let est = X86CacheMissEstimator::new(X86MicroArch::Skylake);
assert_eq!(est.l1_miss_penalty, 10);
assert_eq!(est.dram_penalty, 200);
}
#[test]
fn test_cache_estimator_zen5() {
let est = X86CacheMissEstimator::new(X86MicroArch::Zen5);
assert_eq!(est.dram_penalty, 140);
}
#[test]
fn test_cache_miss_penalty_small() {
let est = X86CacheMissEstimator::new(X86MicroArch::Skylake);
let penalty = est.estimate_miss_penalty(100, 64, 64);
assert!(penalty > 0.0);
}
#[test]
fn test_cache_compare_layouts() {
let est = X86CacheMissEstimator::new(X86MicroArch::Skylake);
let cmp = est.compare_layouts(100.0, 50.0, 200.0, 300.0);
assert_eq!(cmp, -1);
}
#[test]
fn test_transform_type_names() {
assert_eq!(LoopTransformType::Rotate.name(), "rotate");
assert_eq!(LoopTransformType::FullUnroll.name(), "full-unroll");
assert_eq!(LoopTransformType::Simplify.name(), "simplify");
assert_eq!(LoopTransformType::Align.name(), "align");
}
#[test]
fn test_pipeline_stage_creation() {
let stage = X86LoopPipelineStage::new("test-stage", 42, false);
assert_eq!(stage.name, "test-stage");
assert_eq!(stage.order, 42);
assert!(!stage.is_mir_pass);
}
#[test]
fn test_pipeline_stage_with_transform() {
let stage = X86LoopPipelineStage::new("test", 0, false)
.with_transform(LoopTransformType::Rotate)
.with_transform(LoopTransformType::Simplify);
assert_eq!(stage.enabled_transforms.len(), 2);
}
#[test]
fn test_pipeline_builder_standard() {
let subtarget = make_test_subtarget();
let mut builder = X86LoopPipelineBuilder::new(subtarget);
builder.build_standard_pipeline();
assert!(!builder.stages.is_empty());
assert!(builder.stages.len() >= 7);
}
#[test]
fn test_pipeline_builder_aggressive() {
let subtarget = make_test_subtarget();
let mut builder = X86LoopPipelineBuilder::new(subtarget);
builder.build_aggressive_pipeline();
assert!(builder.stages.len() >= 8);
}
#[test]
fn test_pipeline_builder_size() {
let subtarget = make_test_subtarget();
let mut builder = X86LoopPipelineBuilder::new(subtarget);
builder.build_size_pipeline();
assert!(builder.stages.len() >= 2);
}
#[test]
fn test_pipeline_builder_run() {
let subtarget = make_test_subtarget();
let mut builder = X86LoopPipelineBuilder::new(subtarget);
builder.build_standard_pipeline();
let (func, blocks, pred_map, succ_map) = make_nested_loop_cfg();
let stats = builder.run_on_function(&func, &blocks, &pred_map, &succ_map);
assert!(builder.optimizer.loops_analyzed >= 0);
}
#[test]
fn test_pipeline_builder_reset() {
let subtarget = make_test_subtarget();
let mut builder = X86LoopPipelineBuilder::new(subtarget);
let (func, blocks, pred_map, succ_map) = make_loop_cfg();
builder.build_standard_pipeline();
builder.run_on_function(&func, &blocks, &pred_map, &succ_map);
builder.reset();
assert!(builder.optimizer.loops.is_empty());
}
#[test]
fn test_pipeline_builder_stats() {
let subtarget = make_test_subtarget();
let builder = X86LoopPipelineBuilder::new(subtarget);
let stats = builder.stats();
assert!(!stats.made_progress());
}
}