use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InterleaveIsa {
SSE2,
SSE3,
SSSE3,
SSE41,
SSE42,
AVX,
AVX2,
AVX512F,
AVX512BW,
AVX512VL,
}
impl fmt::Display for InterleaveIsa {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
InterleaveIsa::SSE2 => "sse2",
InterleaveIsa::SSE3 => "sse3",
InterleaveIsa::SSSE3 => "ssse3",
InterleaveIsa::SSE41 => "sse4.1",
InterleaveIsa::SSE42 => "sse4.2",
InterleaveIsa::AVX => "avx",
InterleaveIsa::AVX2 => "avx2",
InterleaveIsa::AVX512F => "avx512f",
InterleaveIsa::AVX512BW => "avx512bw",
InterleaveIsa::AVX512VL => "avx512vl",
};
write!(f, "{}", s)
}
}
impl InterleaveIsa {
pub fn vector_width(&self) -> u32 {
match self {
InterleaveIsa::SSE2
| InterleaveIsa::SSE3
| InterleaveIsa::SSSE3
| InterleaveIsa::SSE41
| InterleaveIsa::SSE42 => 128,
InterleaveIsa::AVX | InterleaveIsa::AVX2 => 256,
InterleaveIsa::AVX512F | InterleaveIsa::AVX512BW | InterleaveIsa::AVX512VL => 512,
}
}
pub fn has_3_operand_shuffle(&self) -> bool {
matches!(
self,
InterleaveIsa::AVX
| InterleaveIsa::AVX2
| InterleaveIsa::AVX512F
| InterleaveIsa::AVX512BW
| InterleaveIsa::AVX512VL
)
}
pub fn has_cross_lane_permute(&self) -> bool {
matches!(
self,
InterleaveIsa::AVX2
| InterleaveIsa::AVX512F
| InterleaveIsa::AVX512BW
| InterleaveIsa::AVX512VL
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InterleavedAccessGroup {
pub base_ptr: String,
pub stride: u32,
pub num_members: u32,
pub element_size: u32,
pub is_load: bool,
pub alignment: u32,
pub isa: InterleaveIsa,
}
impl InterleavedAccessGroup {
pub fn new_load(
base: &str,
stride: u32,
num_members: u32,
elem_size: u32,
alignment: u32,
isa: InterleaveIsa,
) -> Self {
Self {
base_ptr: base.into(),
stride,
num_members,
element_size: elem_size,
is_load: true,
alignment,
isa,
}
}
pub fn new_store(
base: &str,
stride: u32,
num_members: u32,
elem_size: u32,
alignment: u32,
isa: InterleaveIsa,
) -> Self {
Self {
base_ptr: base.into(),
stride,
num_members,
element_size: elem_size,
is_load: false,
alignment,
isa,
}
}
pub fn total_elements(&self) -> u32 {
self.num_members
}
pub fn bytes_per_member(&self) -> u32 {
self.num_members * self.element_size
}
pub fn total_bytes(&self) -> u32 {
self.bytes_per_member() * self.stride
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InterleaveElementType {
I8,
I16,
I32,
I64,
F32,
F64,
}
impl InterleaveElementType {
pub fn bits(&self) -> u32 {
match self {
InterleaveElementType::I8 => 8,
InterleaveElementType::I16 => 16,
InterleaveElementType::I32 | InterleaveElementType::F32 => 32,
InterleaveElementType::I64 | InterleaveElementType::F64 => 64,
}
}
pub fn size_bytes(&self) -> u32 {
self.bits() / 8
}
pub fn is_float(&self) -> bool {
matches!(
self,
InterleaveElementType::F32 | InterleaveElementType::F64
)
}
}
impl fmt::Display for InterleaveElementType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
InterleaveElementType::I8 => "i8",
InterleaveElementType::I16 => "i16",
InterleaveElementType::I32 => "i32",
InterleaveElementType::I64 => "i64",
InterleaveElementType::F32 => "f32",
InterleaveElementType::F64 => "f64",
};
write!(f, "{}", s)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShuffleStep {
pub mnemonic: String,
pub sources: Vec<String>,
pub dest: String,
pub immediate: Option<u8>,
pub is_destructive: bool,
}
impl ShuffleStep {
pub fn new(mnemonic: &str, dest: &str, sources: Vec<String>) -> Self {
Self {
mnemonic: mnemonic.into(),
sources,
dest: dest.into(),
immediate: None,
is_destructive: true,
}
}
pub fn with_imm(mut self, imm: u8) -> Self {
self.immediate = Some(imm);
self
}
pub fn nondestructive(mut self) -> Self {
self.is_destructive = false;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoweringSequence {
pub group: InterleavedAccessGroup,
pub steps: Vec<ShuffleStep>,
pub estimated_cost: u32,
pub is_lowered: bool,
pub description: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InterleaveCost {
pub interleaved_cost: u32,
pub scalar_cost: u32,
pub prefer_interleaved: bool,
pub vector_memory_ops: u32,
pub shuffle_count: u32,
pub total_instructions: u32,
}
impl InterleaveCost {
pub fn speedup_factor(&self) -> f64 {
if self.scalar_cost == 0 {
f64::INFINITY
} else {
self.scalar_cost as f64 / self.interleaved_cost as f64
}
}
}
#[derive(Debug)]
pub struct X86InterleavedAccess {
pub isa: InterleaveIsa,
pub groups: Vec<InterleavedAccessGroup>,
pub sequences: Vec<LoweringSequence>,
pub cost_data: HashMap<String, InterleaveCost>,
pub groups_lowered: u32,
pub groups_rejected: u32,
pub stats: InterleaveStats,
pub config: InterleaveConfig,
}
#[derive(Debug, Clone, Default)]
pub struct InterleaveStats {
pub stride2_lowered: u32,
pub stride3_lowered: u32,
pub stride4_lowered: u32,
pub stride_other_lowered: u32,
pub loads_lowered: u32,
pub stores_lowered: u32,
pub avx_lowered: u32,
pub avx512_lowered: u32,
pub sse_lowered: u32,
pub rejected_profitability: u32,
pub rejected_unsupported: u32,
}
impl InterleaveStats {
pub fn total_lowered(&self) -> u32 {
self.stride2_lowered
+ self.stride3_lowered
+ self.stride4_lowered
+ self.stride_other_lowered
}
pub fn merge(&mut self, other: &InterleaveStats) {
self.stride2_lowered += other.stride2_lowered;
self.stride3_lowered += other.stride3_lowered;
self.stride4_lowered += other.stride4_lowered;
self.stride_other_lowered += other.stride_other_lowered;
self.loads_lowered += other.loads_lowered;
self.stores_lowered += other.stores_lowered;
self.avx_lowered += other.avx_lowered;
self.avx512_lowered += other.avx512_lowered;
self.sse_lowered += other.sse_lowered;
self.rejected_profitability += other.rejected_profitability;
self.rejected_unsupported += other.rejected_unsupported;
}
}
#[derive(Debug, Clone)]
pub struct InterleaveConfig {
pub max_stride: u32,
pub min_members: u32,
pub min_speedup: f64,
pub vector_load_cost: u32,
pub vector_store_cost: u32,
pub in_lane_shuffle_cost: u32,
pub cross_lane_shuffle_cost: u32,
pub enable_stride3: bool,
pub prefer_interleaved: bool,
}
impl Default for InterleaveConfig {
fn default() -> Self {
Self {
max_stride: 4,
min_members: 2,
min_speedup: 1.0,
vector_load_cost: 4,
vector_store_cost: 4,
in_lane_shuffle_cost: 1,
cross_lane_shuffle_cost: 3,
enable_stride3: true,
prefer_interleaved: true,
}
}
}
impl X86InterleavedAccess {
pub fn new(isa: InterleaveIsa) -> Self {
Self {
isa,
groups: Vec::new(),
sequences: Vec::new(),
cost_data: HashMap::new(),
groups_lowered: 0,
groups_rejected: 0,
stats: InterleaveStats::default(),
config: InterleaveConfig::default(),
}
}
pub fn with_config(mut self, config: InterleaveConfig) -> Self {
self.config = config;
self
}
pub fn register_group(&mut self, group: InterleavedAccessGroup) {
self.groups.push(group);
}
pub fn register_load(
&mut self,
base: &str,
stride: u32,
num_members: u32,
elem_bytes: u32,
alignment: u32,
) {
let group = InterleavedAccessGroup::new_load(
base,
stride,
num_members,
elem_bytes,
alignment,
self.isa,
);
self.groups.push(group);
}
pub fn register_store(
&mut self,
base: &str,
stride: u32,
num_members: u32,
elem_bytes: u32,
alignment: u32,
) {
let group = InterleavedAccessGroup::new_store(
base,
stride,
num_members,
elem_bytes,
alignment,
self.isa,
);
self.groups.push(group);
}
pub fn lower_all(&mut self) -> Vec<LoweringSequence> {
let groups: Vec<_> = self.groups.drain(..).collect();
for group in groups {
let seq = self.lower_group(&group);
let lowered = seq.is_lowered;
self.sequences.push(seq.clone());
if lowered {
self.groups_lowered += 1;
self.track_stats(&group);
} else {
self.groups_rejected += 1;
if !self.is_stride_supported(group.stride) {
self.stats.rejected_unsupported += 1;
} else {
self.stats.rejected_profitability += 1;
}
}
self.cost_data
.insert(group.base_ptr.clone(), self.cost_for_group(&group));
}
self.sequences.clone()
}
pub fn lower_group(&mut self, group: &InterleavedAccessGroup) -> LoweringSequence {
if !self.is_stride_supported(group.stride) {
return LoweringSequence {
group: group.clone(),
steps: vec![],
estimated_cost: u32::MAX,
is_lowered: false,
description: format!(
"unsupported stride {} for {}",
group.stride,
if group.is_load { "load" } else { "store" }
),
};
}
let cost = self.cost_for_group(group);
if !cost.prefer_interleaved {
return LoweringSequence {
group: group.clone(),
steps: vec![],
estimated_cost: cost.scalar_cost,
is_lowered: false,
description: format!(
"interleaved not profitable (speedup {:.2} < {:.2})",
cost.speedup_factor(),
self.config.min_speedup
),
};
}
if group.is_load {
self.lower_interleaved_load(group)
} else {
self.lower_interleaved_store(group)
}
}
fn lower_interleaved_load(&self, group: &InterleavedAccessGroup) -> LoweringSequence {
let mut steps = Vec::new();
let elem = group.element_size;
match group.stride {
2 => self.deinterleave_stride_2_load(group, &mut steps, elem),
3 => {
if self.config.enable_stride3 {
self.deinterleave_stride_3_load(group, &mut steps, elem)
} else {
return unsupported(group, "stride-3 disabled");
}
}
4 => self.deinterleave_stride_4_load(group, &mut steps, elem),
_ => self.deinterleave_stride_n_load(group, &mut steps, elem),
}
let total_cost = self.compute_sequence_cost(&steps, group.is_load);
let desc = format!(
"interleaved load: stride={}, members={}, elem_bytes={}, steps={}, cost={}",
group.stride,
group.num_members,
group.element_size,
steps.len(),
total_cost
);
LoweringSequence {
group: group.clone(),
steps,
estimated_cost: total_cost,
is_lowered: true,
description: desc,
}
}
fn deinterleave_stride_2_load(
&self,
group: &InterleavedAccessGroup,
steps: &mut Vec<ShuffleStep>,
_elem: u32,
) {
let n = group.num_members;
let even_vec = format!("{}.even", group.base_ptr);
let odd_vec = format!("{}.odd", group.base_ptr);
steps.push(ShuffleStep::new(
"vmovdqu",
&even_vec,
vec![group.base_ptr.clone()],
));
steps.push(ShuffleStep::new(
"vmovdqu",
&odd_vec,
vec![format!("{}+{}", group.base_ptr, group.element_size * n)],
));
let lo = format!("{}.lo", group.base_ptr);
let hi = format!("{}.hi", group.base_ptr);
let suffix = if self.isa.has_3_operand_shuffle() {
"v"
} else {
""
};
if group.element_size == 4 {
steps.push(
ShuffleStep::new(
&format!("{}unpcklps", suffix),
&lo,
vec![even_vec.clone(), odd_vec.clone()],
)
.nondestructive(),
);
steps.push(
ShuffleStep::new(&format!("{}unpckhps", suffix), &hi, vec![even_vec, odd_vec])
.nondestructive(),
);
} else if group.element_size == 8 {
steps.push(
ShuffleStep::new(
&format!("{}unpcklpd", suffix),
&lo,
vec![even_vec.clone(), odd_vec.clone()],
)
.nondestructive(),
);
steps.push(
ShuffleStep::new(&format!("{}unpckhpd", suffix), &hi, vec![even_vec, odd_vec])
.nondestructive(),
);
} else {
let pfx = if self.isa.has_3_operand_shuffle() {
"vp"
} else {
"p"
};
let bw = match group.element_size {
1 => "bw",
2 => "wd",
4 => "dq",
_ => "dq",
};
steps.push(
ShuffleStep::new(
&format!("{}unpckl{}", pfx, bw),
&lo,
vec![even_vec.clone(), odd_vec.clone()],
)
.nondestructive(),
);
steps.push(
ShuffleStep::new(
&format!("{}unpckh{}", pfx, bw),
&hi,
vec![even_vec, odd_vec],
)
.nondestructive(),
);
}
}
fn deinterleave_stride_3_load(
&self,
group: &InterleavedAccessGroup,
steps: &mut Vec<ShuffleStep>,
_elem: u32,
) {
let n = group.num_members;
let v0 = format!("{}.v0", group.base_ptr);
let v1 = format!("{}.v1", group.base_ptr);
let v2 = format!("{}.v2", group.base_ptr);
steps.push(ShuffleStep::new(
"vmovdqu",
&v0,
vec![group.base_ptr.clone()],
));
steps.push(ShuffleStep::new(
"vmovdqu",
&v1,
vec![format!("{}+{}", group.base_ptr, group.element_size * n)],
));
steps.push(ShuffleStep::new(
"vmovdqu",
&v2,
vec![format!("{}+{}", group.base_ptr, 2 * group.element_size * n)],
));
let lane0 = format!("{}.lane0", group.base_ptr);
let lane1 = format!("{}.lane1", group.base_ptr);
let lane2 = format!("{}.lane2", group.base_ptr);
if group.element_size == 4 && self.isa.has_3_operand_shuffle() {
steps.push(
ShuffleStep::new("vshufps", &lane0, vec![v0.clone(), v1.clone()]).with_imm(0x88),
); steps.push(
ShuffleStep::new("vshufps", &lane1, vec![v0.clone(), v2.clone()]).with_imm(0xDD),
);
steps.push(ShuffleStep::new("vshufps", &lane2, vec![v1, v2]).with_imm(0x88));
} else {
steps.push(
ShuffleStep::new("vpblendw", &lane0, vec![v0.clone(), v1.clone()]).with_imm(0x33),
);
steps.push(
ShuffleStep::new("vpblendw", &lane1, vec![v1.clone(), v2.clone()]).with_imm(0x66),
);
steps.push(ShuffleStep::new("vpblendw", &lane2, vec![v2, v0]).with_imm(0xCC));
}
}
fn deinterleave_stride_4_load(
&self,
group: &InterleavedAccessGroup,
steps: &mut Vec<ShuffleStep>,
_elem: u32,
) {
let n = group.num_members;
let v0 = format!("{}.v0", group.base_ptr);
let v1 = format!("{}.v1", group.base_ptr);
let v2 = format!("{}.v2", group.base_ptr);
let v3 = format!("{}.v3", group.base_ptr);
steps.push(ShuffleStep::new(
"vmovdqu",
&v0,
vec![group.base_ptr.clone()],
));
steps.push(ShuffleStep::new(
"vmovdqu",
&v1,
vec![format!("{}+{}", group.base_ptr, group.element_size * n)],
));
steps.push(ShuffleStep::new(
"vmovdqu",
&v2,
vec![format!("{}+{}", group.base_ptr, 2 * group.element_size * n)],
));
steps.push(ShuffleStep::new(
"vmovdqu",
&v3,
vec![format!("{}+{}", group.base_ptr, 3 * group.element_size * n)],
));
let tmp0 = format!("{}.t0", group.base_ptr);
let tmp1 = format!("{}.t1", group.base_ptr);
let tmp2 = format!("{}.t2", group.base_ptr);
let tmp3 = format!("{}.t3", group.base_ptr);
if group.element_size == 4 && self.isa.has_3_operand_shuffle() {
steps.push(
ShuffleStep::new("vunpcklps", &tmp0, vec![v0.clone(), v1.clone()]).nondestructive(),
);
steps.push(ShuffleStep::new("vunpckhps", &tmp1, vec![v0, v1]).nondestructive());
steps.push(
ShuffleStep::new("vunpcklps", &tmp2, vec![v2.clone(), v3.clone()]).nondestructive(),
);
steps.push(ShuffleStep::new("vunpckhps", &tmp3, vec![v2, v3]).nondestructive());
let lane0 = format!("{}.lane0", group.base_ptr);
let lane1 = format!("{}.lane1", group.base_ptr);
let lane2 = format!("{}.lane2", group.base_ptr);
let lane3 = format!("{}.lane3", group.base_ptr);
steps.push(
ShuffleStep::new("vshufps", &lane0, vec![tmp0.clone(), tmp2.clone()])
.with_imm(0x88),
);
steps.push(ShuffleStep::new("vshufps", &lane1, vec![tmp0, tmp2]).with_imm(0xDD));
steps.push(
ShuffleStep::new("vshufps", &lane2, vec![tmp1.clone(), tmp3.clone()])
.with_imm(0x88),
);
steps.push(ShuffleStep::new("vshufps", &lane3, vec![tmp1, tmp3]).with_imm(0xDD));
} else {
steps.push(ShuffleStep::new(
"punpckldq",
&tmp0,
vec![v0.clone(), v1.clone()],
));
steps.push(ShuffleStep::new("punpckhdq", &tmp1, vec![v0, v1]));
steps.push(ShuffleStep::new(
"punpckldq",
&tmp2,
vec![v2.clone(), v3.clone()],
));
steps.push(ShuffleStep::new("punpckhdq", &tmp3, vec![v2, v3]));
}
}
fn deinterleave_stride_n_load(
&self,
group: &InterleavedAccessGroup,
steps: &mut Vec<ShuffleStep>,
_elem: u32,
) {
for m in 0..group.num_members {
let off = m * group.element_size * group.num_members;
let v = format!("{}.v{}", group.base_ptr, m);
steps.push(ShuffleStep::new(
"vmovdqu",
&v,
vec![format!("{}+{}", group.base_ptr, off)],
));
}
for i in 0..group.stride {
let lane = format!("{}.lane{}", group.base_ptr, i);
steps.push(
ShuffleStep::new("vpermd", &lane, vec![format!("{}.v0", group.base_ptr)])
.with_imm(i as u8),
);
}
}
fn lower_interleaved_store(&self, group: &InterleavedAccessGroup) -> LoweringSequence {
let mut steps = Vec::new();
let elem = group.element_size;
match group.stride {
2 => self.interleave_stride_2_store(group, &mut steps, elem),
3 => {
if self.config.enable_stride3 {
self.interleave_stride_3_store(group, &mut steps, elem)
} else {
return unsupported(group, "stride-3 disabled");
}
}
4 => self.interleave_stride_4_store(group, &mut steps, elem),
_ => self.interleave_stride_n_store(group, &mut steps, elem),
}
let total_cost = self.compute_sequence_cost(&steps, group.is_load);
let desc = format!(
"interleaved store: stride={}, members={}, elem_bytes={}, steps={}, cost={}",
group.stride,
group.num_members,
group.element_size,
steps.len(),
total_cost
);
LoweringSequence {
group: group.clone(),
steps,
estimated_cost: total_cost,
is_lowered: true,
description: desc,
}
}
fn interleave_stride_2_store(
&self,
group: &InterleavedAccessGroup,
steps: &mut Vec<ShuffleStep>,
_elem: u32,
) {
let even = format!("{}.even", group.base_ptr);
let odd = format!("{}.odd", group.base_ptr);
let lo = format!("{}.lo", group.base_ptr);
let hi = format!("{}.hi", group.base_ptr);
let suffix = if self.isa.has_3_operand_shuffle() {
"v"
} else {
""
};
if group.element_size == 4 {
steps.push(
ShuffleStep::new(
&format!("{}unpcklps", suffix),
&lo,
vec![even.clone(), odd.clone()],
)
.nondestructive(),
);
steps.push(
ShuffleStep::new(&format!("{}unpckhps", suffix), &hi, vec![even, odd])
.nondestructive(),
);
} else if group.element_size == 8 {
steps.push(
ShuffleStep::new(
&format!("{}unpcklpd", suffix),
&lo,
vec![even.clone(), odd.clone()],
)
.nondestructive(),
);
steps.push(
ShuffleStep::new(&format!("{}unpckhpd", suffix), &hi, vec![even, odd])
.nondestructive(),
);
} else {
let pfx = if self.isa.has_3_operand_shuffle() {
"vp"
} else {
"p"
};
steps.push(
ShuffleStep::new(
&format!("{}unpckldq", pfx),
&lo,
vec![even.clone(), odd.clone()],
)
.nondestructive(),
);
steps.push(
ShuffleStep::new(&format!("{}unpckhdq", pfx), &hi, vec![even, odd])
.nondestructive(),
);
}
steps.push(ShuffleStep::new(
"vmovdqu",
"mem",
vec![lo, group.base_ptr.clone()],
));
steps.push(ShuffleStep::new(
"vmovdqu",
"mem",
vec![
hi,
format!(
"{}+{}",
group.base_ptr,
group.num_members * group.element_size
),
],
));
}
fn interleave_stride_3_store(
&self,
group: &InterleavedAccessGroup,
steps: &mut Vec<ShuffleStep>,
_elem: u32,
) {
let v0 = format!("{}.v0", group.base_ptr);
let v1 = format!("{}.v1", group.base_ptr);
let v2 = format!("{}.v2", group.base_ptr);
let tmp0 = format!("{}.t0", group.base_ptr);
let tmp1 = format!("{}.t1", group.base_ptr);
let tmp2 = format!("{}.t2", group.base_ptr);
steps
.push(ShuffleStep::new("vpblendw", &tmp0, vec![v0.clone(), v1.clone()]).with_imm(0x33));
steps.push(ShuffleStep::new("vpblendw", &tmp1, vec![v1, v2.clone()]).with_imm(0x66));
steps.push(ShuffleStep::new("vpblendw", &tmp2, vec![v2, v0]).with_imm(0xCC));
steps.push(ShuffleStep::new(
"vmovdqu",
"mem",
vec![tmp0, group.base_ptr.clone()],
));
steps.push(ShuffleStep::new(
"vmovdqu",
"mem",
vec![
tmp1,
format!(
"{}+{}",
group.base_ptr,
group.num_members * group.element_size
),
],
));
steps.push(ShuffleStep::new(
"vmovdqu",
"mem",
vec![
tmp2,
format!(
"{}+{}",
group.base_ptr,
2 * group.num_members * group.element_size
),
],
));
}
fn interleave_stride_4_store(
&self,
group: &InterleavedAccessGroup,
steps: &mut Vec<ShuffleStep>,
_elem: u32,
) {
let v0 = format!("{}.v0", group.base_ptr);
let v1 = format!("{}.v1", group.base_ptr);
let v2 = format!("{}.v2", group.base_ptr);
let v3 = format!("{}.v3", group.base_ptr);
let tmp0 = format!("{}.t0", group.base_ptr);
let tmp1 = format!("{}.t1", group.base_ptr);
let tmp2 = format!("{}.t2", group.base_ptr);
let tmp3 = format!("{}.t3", group.base_ptr);
if group.element_size == 4 {
steps.push(
ShuffleStep::new("vunpcklps", &tmp0, vec![v0.clone(), v1.clone()]).nondestructive(),
);
steps.push(ShuffleStep::new("vunpckhps", &tmp1, vec![v0, v1]).nondestructive());
steps.push(
ShuffleStep::new("vunpcklps", &tmp2, vec![v2.clone(), v3.clone()]).nondestructive(),
);
steps.push(ShuffleStep::new("vunpckhps", &tmp3, vec![v2, v3]).nondestructive());
let s0 = format!("{}.s0", group.base_ptr);
let s1 = format!("{}.s1", group.base_ptr);
let s2 = format!("{}.s2", group.base_ptr);
let s3 = format!("{}.s3", group.base_ptr);
steps.push(
ShuffleStep::new("vshufps", &s0, vec![tmp0.clone(), tmp2.clone()]).with_imm(0x88),
);
steps.push(
ShuffleStep::new("vshufps", &s1, vec![tmp1.clone(), tmp3.clone()]).with_imm(0x88),
);
steps.push(ShuffleStep::new("vshufps", &s2, vec![tmp0, tmp2]).with_imm(0xDD));
steps.push(ShuffleStep::new("vshufps", &s3, vec![tmp1, tmp3]).with_imm(0xDD));
steps.push(ShuffleStep::new(
"vmovdqu",
"mem",
vec![s0, group.base_ptr.clone()],
));
steps.push(ShuffleStep::new(
"vmovdqu",
"mem",
vec![
s1,
format!(
"{}+{}",
group.base_ptr,
group.num_members * group.element_size
),
],
));
steps.push(ShuffleStep::new(
"vmovdqu",
"mem",
vec![
s2,
format!(
"{}+{}",
group.base_ptr,
2 * group.num_members * group.element_size
),
],
));
steps.push(ShuffleStep::new(
"vmovdqu",
"mem",
vec![
s3,
format!(
"{}+{}",
group.base_ptr,
3 * group.num_members * group.element_size
),
],
));
}
}
fn interleave_stride_n_store(
&self,
group: &InterleavedAccessGroup,
steps: &mut Vec<ShuffleStep>,
_elem: u32,
) {
for m in 0..group.stride {
let s = format!("{}.s{}", group.base_ptr, m);
steps.push(
ShuffleStep::new("vpermilps", &s, vec![format!("{}.v0", group.base_ptr)])
.with_imm(m as u8),
);
let off = m * group.num_members * group.element_size;
steps.push(ShuffleStep::new(
"vmovdqu",
"mem",
vec![s, format!("{}+{}", group.base_ptr, off)],
));
}
}
pub fn cost_for_group(&self, group: &InterleavedAccessGroup) -> InterleaveCost {
let n = group.num_members;
let stride = group.stride;
let scalar_mem_ops = n * stride;
let scalar_cost = scalar_mem_ops
* if group.is_load {
self.config.vector_load_cost
} else {
self.config.vector_store_cost
};
let vector_mem_ops = stride; let mut shuffle_count: u32 = match stride {
2 => 2, 3 => 4, 4 => 8, _ => stride * 2,
};
if !group.is_load {
shuffle_count += stride;
}
let shuffle_cost = if group.isa.has_cross_lane_permute() {
shuffle_count * self.config.in_lane_shuffle_cost
} else {
shuffle_count * self.config.cross_lane_shuffle_cost
};
let mem_cost = vector_mem_ops
* if group.is_load {
self.config.vector_load_cost
} else {
self.config.vector_store_cost
};
let interleaved_cost = mem_cost + shuffle_cost;
let total_instructions = vector_mem_ops + shuffle_count;
let prefer = self.config.prefer_interleaved
&& (interleaved_cost as f64) < (scalar_cost as f64) * self.config.min_speedup;
InterleaveCost {
interleaved_cost,
scalar_cost,
prefer_interleaved: prefer,
vector_memory_ops: vector_mem_ops,
shuffle_count,
total_instructions,
}
}
pub fn quick_cost(&self, stride: u32, num_members: u32, is_load: bool) -> InterleaveCost {
let group = InterleavedAccessGroup {
base_ptr: "tmp".into(),
stride,
num_members,
element_size: 4,
is_load,
alignment: 16,
isa: self.isa,
};
self.cost_for_group(&group)
}
fn compute_sequence_cost(&self, steps: &[ShuffleStep], is_load: bool) -> u32 {
let mut cost = 0u32;
for step in steps {
if step.mnemonic.contains("vmovdqu") || step.mnemonic.contains("movdqu") {
cost += if is_load {
self.config.vector_load_cost
} else {
self.config.vector_store_cost
};
} else if step.mnemonic.contains("perm")
|| step.mnemonic.contains("shuf")
|| step.mnemonic.contains("vperm2")
|| step.mnemonic.contains("vshuff32")
{
cost += self.config.cross_lane_shuffle_cost;
} else {
cost += self.config.in_lane_shuffle_cost;
}
}
cost
}
pub fn is_stride_supported(&self, stride: u32) -> bool {
if stride < 1 {
return false;
}
if stride > self.config.max_stride {
return false;
}
if stride == 3 && !self.config.enable_stride3 {
return false;
}
true
}
pub fn is_candidate(&self, group: &InterleavedAccessGroup) -> bool {
group.num_members >= self.config.min_members
&& self.is_stride_supported(group.stride)
&& group.element_size > 0
}
fn track_stats(&mut self, group: &InterleavedAccessGroup) {
match group.stride {
2 => self.stats.stride2_lowered += 1,
3 => self.stats.stride3_lowered += 1,
4 => self.stats.stride4_lowered += 1,
_ => self.stats.stride_other_lowered += 1,
}
if group.is_load {
self.stats.loads_lowered += 1;
} else {
self.stats.stores_lowered += 1;
}
match group.isa {
InterleaveIsa::SSE2
| InterleaveIsa::SSE3
| InterleaveIsa::SSSE3
| InterleaveIsa::SSE41
| InterleaveIsa::SSE42 => self.stats.sse_lowered += 1,
InterleaveIsa::AVX | InterleaveIsa::AVX2 => self.stats.avx_lowered += 1,
InterleaveIsa::AVX512F | InterleaveIsa::AVX512BW | InterleaveIsa::AVX512VL => {
self.stats.avx512_lowered += 1;
}
}
}
pub fn summary(&self) -> String {
format!(
"X86InterleavedAccess: total_lowered={} (sse={}, avx={}, avx512={}) \
loads={} stores={} stride2={} stride3={} stride4={} rejected={}",
self.stats.total_lowered(),
self.stats.sse_lowered,
self.stats.avx_lowered,
self.stats.avx512_lowered,
self.stats.loads_lowered,
self.stats.stores_lowered,
self.stats.stride2_lowered,
self.stats.stride3_lowered,
self.stats.stride4_lowered,
self.groups_rejected,
)
}
pub fn reset(&mut self) {
self.groups.clear();
self.sequences.clear();
self.cost_data.clear();
self.groups_lowered = 0;
self.groups_rejected = 0;
self.stats = InterleaveStats::default();
}
}
fn unsupported(group: &InterleavedAccessGroup, reason: &str) -> LoweringSequence {
LoweringSequence {
group: group.clone(),
steps: vec![],
estimated_cost: u32::MAX,
is_lowered: false,
description: reason.into(),
}
}
pub fn make_x86_interleaved_access_sse2() -> X86InterleavedAccess {
X86InterleavedAccess::new(InterleaveIsa::SSE2)
}
pub fn make_x86_interleaved_access_avx2() -> X86InterleavedAccess {
X86InterleavedAccess::new(InterleaveIsa::AVX2)
}
pub fn make_x86_interleaved_access_avx512() -> X86InterleavedAccess {
X86InterleavedAccess::new(InterleaveIsa::AVX512F)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_sse2() -> X86InterleavedAccess {
X86InterleavedAccess::new(InterleaveIsa::SSE2)
}
fn make_avx2() -> X86InterleavedAccess {
X86InterleavedAccess::new(InterleaveIsa::AVX2)
}
fn make_avx512() -> X86InterleavedAccess {
X86InterleavedAccess::new(InterleaveIsa::AVX512F)
}
#[test]
fn test_isa_vector_width() {
assert_eq!(InterleaveIsa::SSE2.vector_width(), 128);
assert_eq!(InterleaveIsa::AVX2.vector_width(), 256);
assert_eq!(InterleaveIsa::AVX512F.vector_width(), 512);
}
#[test]
fn test_isa_has_3_operand_shuffle() {
assert!(!InterleaveIsa::SSE2.has_3_operand_shuffle());
assert!(InterleaveIsa::AVX.has_3_operand_shuffle());
assert!(InterleaveIsa::AVX512F.has_3_operand_shuffle());
}
#[test]
fn test_isa_has_cross_lane_permute() {
assert!(!InterleaveIsa::SSE2.has_cross_lane_permute());
assert!(InterleaveIsa::AVX2.has_cross_lane_permute());
assert!(InterleaveIsa::AVX512VL.has_cross_lane_permute());
}
#[test]
fn test_isa_display() {
assert_eq!(format!("{}", InterleaveIsa::AVX2), "avx2");
assert_eq!(format!("{}", InterleaveIsa::AVX512VL), "avx512vl");
}
#[test]
fn test_group_new_load() {
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
assert!(g.is_load);
assert_eq!(g.stride, 2);
assert_eq!(g.num_members, 4);
assert_eq!(g.total_elements(), 4);
}
#[test]
fn test_group_new_store() {
let g = InterleavedAccessGroup::new_store("buf", 4, 2, 8, 32, InterleaveIsa::AVX512F);
assert!(!g.is_load);
assert_eq!(g.bytes_per_member(), 16);
assert_eq!(g.total_bytes(), 64);
}
#[test]
fn test_group_bytes() {
let g = InterleavedAccessGroup::new_load("b", 2, 4, 4, 16, InterleaveIsa::AVX2);
assert_eq!(g.bytes_per_member(), 16);
assert_eq!(g.total_bytes(), 32);
}
#[test]
fn test_element_bits() {
assert_eq!(InterleaveElementType::I32.bits(), 32);
assert_eq!(InterleaveElementType::F64.bits(), 64);
}
#[test]
fn test_element_is_float() {
assert!(InterleaveElementType::F32.is_float());
assert!(!InterleaveElementType::I64.is_float());
}
#[test]
fn test_element_size_bytes() {
assert_eq!(InterleaveElementType::I8.size_bytes(), 1);
assert_eq!(InterleaveElementType::F64.size_bytes(), 8);
}
#[test]
fn test_element_display() {
assert_eq!(format!("{}", InterleaveElementType::I32), "i32");
assert_eq!(format!("{}", InterleaveElementType::F32), "f32");
}
#[test]
fn test_shuffle_step_new() {
let s = ShuffleStep::new("unpcklps", "dest", vec!["a".into(), "b".into()]);
assert_eq!(s.mnemonic, "unpcklps");
assert_eq!(s.sources.len(), 2);
assert!(s.is_destructive);
}
#[test]
fn test_shuffle_step_with_imm() {
let s = ShuffleStep::new("vshufps", "d", vec!["a".into()]).with_imm(0x88);
assert_eq!(s.immediate, Some(0x88));
}
#[test]
fn test_shuffle_step_nondestructive() {
let s = ShuffleStep::new("vunpcklps", "d", vec!["a".into(), "b".into()]).nondestructive();
assert!(!s.is_destructive);
}
#[test]
fn test_engine_new_sse2() {
let engine = make_sse2();
assert_eq!(engine.isa, InterleaveIsa::SSE2);
assert_eq!(engine.groups_lowered, 0);
}
#[test]
fn test_engine_new_avx2() {
let engine = make_avx2();
assert_eq!(engine.isa, InterleaveIsa::AVX2);
}
#[test]
fn test_engine_new_avx512() {
let engine = make_avx512();
assert_eq!(engine.isa, InterleaveIsa::AVX512F);
}
#[test]
fn test_engine_with_config() {
let config = InterleaveConfig {
max_stride: 8,
..Default::default()
};
let engine = make_avx2().with_config(config);
assert_eq!(engine.config.max_stride, 8);
}
#[test]
fn test_register_group() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
engine.register_group(g);
assert_eq!(engine.groups.len(), 1);
}
#[test]
fn test_register_load() {
let mut engine = make_avx2();
engine.register_load("ptr", 2, 2, 4, 16);
assert_eq!(engine.groups.len(), 1);
assert!(engine.groups[0].is_load);
}
#[test]
fn test_register_store() {
let mut engine = make_avx2();
engine.register_store("ptr", 4, 4, 8, 32);
assert_eq!(engine.groups.len(), 1);
assert!(!engine.groups[0].is_load);
}
#[test]
fn test_lower_stride_2_load_avx2() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
assert!(!seq.steps.is_empty());
}
#[test]
fn test_lower_stride_2_store_avx2() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_store("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_lower_stride_2_load_f64() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 2, 8, 32, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_lower_stride_3_load_avx2() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 3, 3, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_lower_stride_3_store_avx2() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_store("buf", 3, 3, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_lower_stride_3_disabled() {
let config = InterleaveConfig {
enable_stride3: false,
..Default::default()
};
let mut engine = make_avx2().with_config(config);
let g = InterleavedAccessGroup::new_load("buf", 3, 3, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(!seq.is_lowered);
}
#[test]
fn test_lower_stride_4_load_avx2() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 4, 4, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_lower_stride_4_store_avx2() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_store("buf", 4, 4, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_lower_all_multiple_groups() {
let mut engine = make_avx2();
engine.register_load("a", 2, 2, 4, 16);
engine.register_load("b", 2, 4, 4, 16);
engine.register_store("c", 4, 4, 8, 32);
let seqs = engine.lower_all();
assert_eq!(seqs.len(), 3);
assert!(seqs.iter().all(|s| s.is_lowered));
}
#[test]
fn test_lower_all_unsupported_stride() {
let mut engine = make_avx2().with_config(InterleaveConfig {
max_stride: 2,
..Default::default()
});
engine.register_load("buf", 4, 4, 4, 16);
let seqs = engine.lower_all();
assert_eq!(seqs.len(), 1);
assert!(!seqs[0].is_lowered);
assert_eq!(engine.groups_rejected, 1);
}
#[test]
fn test_cost_for_group_stride_2_load() {
let engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
let cost = engine.cost_for_group(&g);
assert!(cost.interleaved_cost > 0);
assert!(cost.scalar_cost > 0);
assert!(cost.prefer_interleaved);
}
#[test]
fn test_cost_for_group_stride_4_store() {
let engine = make_avx2();
let g = InterleavedAccessGroup::new_store("buf", 4, 4, 4, 16, InterleaveIsa::AVX2);
let cost = engine.cost_for_group(&g);
assert!(cost.total_instructions > 0);
}
#[test]
fn test_cost_speedup_factor() {
let cost = InterleaveCost {
interleaved_cost: 10,
scalar_cost: 40,
prefer_interleaved: true,
vector_memory_ops: 4,
shuffle_count: 4,
total_instructions: 8,
};
assert_eq!(cost.speedup_factor(), 4.0);
}
#[test]
fn test_cost_speedup_factor_zero() {
let cost = InterleaveCost {
interleaved_cost: 10,
scalar_cost: 0,
prefer_interleaved: true,
vector_memory_ops: 0,
shuffle_count: 0,
total_instructions: 0,
};
assert!(cost.speedup_factor().is_infinite());
}
#[test]
fn test_quick_cost() {
let engine = make_avx2();
let cost = engine.quick_cost(2, 4, true);
assert!(cost.interleaved_cost > 0);
}
#[test]
fn test_quick_cost_store() {
let engine = make_avx2();
let cost = engine.quick_cost(2, 4, false);
assert!(cost.total_instructions > 0);
}
#[test]
fn test_is_stride_supported() {
let engine = make_avx2();
assert!(engine.is_stride_supported(2));
assert!(engine.is_stride_supported(3));
assert!(engine.is_stride_supported(4));
assert!(!engine.is_stride_supported(0));
assert!(!engine.is_stride_supported(5));
}
#[test]
fn test_is_stride_supported_custom_max() {
let engine = make_avx2().with_config(InterleaveConfig {
max_stride: 6,
..Default::default()
});
assert!(engine.is_stride_supported(6));
assert!(!engine.is_stride_supported(7));
}
#[test]
fn test_is_candidate() {
let engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 2, 4, 16, InterleaveIsa::AVX2);
assert!(engine.is_candidate(&g));
}
#[test]
fn test_not_candidate_too_few_members() {
let engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 1, 4, 16, InterleaveIsa::AVX2);
assert!(!engine.is_candidate(&g));
}
#[test]
fn test_not_candidate_zero_elem_size() {
let engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 0, 16, InterleaveIsa::AVX2);
assert!(!engine.is_candidate(&g));
}
#[test]
fn test_stats_tracking() {
let mut engine = make_avx2();
engine.register_load("a", 2, 2, 4, 16);
engine.register_store("b", 3, 3, 4, 16);
engine.register_load("c", 4, 4, 4, 16);
engine.lower_all();
assert_eq!(engine.stats.stride2_lowered, 1);
assert_eq!(engine.stats.stride3_lowered, 1);
assert_eq!(engine.stats.stride4_lowered, 1);
assert_eq!(engine.stats.loads_lowered, 2);
assert_eq!(engine.stats.stores_lowered, 1);
}
#[test]
fn test_stats_avx512_tracking() {
let mut engine = make_avx512();
engine.register_load("a", 2, 4, 4, 64);
engine.lower_all();
assert_eq!(engine.stats.avx512_lowered, 1);
}
#[test]
fn test_stats_total_lowered() {
let mut stats = InterleaveStats::default();
stats.stride2_lowered = 5;
stats.stride4_lowered = 3;
assert_eq!(stats.total_lowered(), 8);
}
#[test]
fn test_stats_merge() {
let mut a = InterleaveStats::default();
a.stride2_lowered = 2;
a.loads_lowered = 2;
let mut b = InterleaveStats::default();
b.stride2_lowered = 3;
b.stores_lowered = 1;
a.merge(&b);
assert_eq!(a.stride2_lowered, 5);
assert_eq!(a.loads_lowered, 2);
assert_eq!(a.stores_lowered, 1);
}
#[test]
fn test_summary() {
let engine = make_avx2();
let s = engine.summary();
assert!(s.contains("X86InterleavedAccess"));
assert!(s.contains("total_lowered="));
}
#[test]
fn test_reset() {
let mut engine = make_avx2();
engine.register_load("a", 2, 2, 4, 16);
engine.lower_all();
assert!(engine.groups_lowered > 0);
engine.reset();
assert_eq!(engine.groups_lowered, 0);
assert!(engine.groups.is_empty());
assert!(engine.sequences.is_empty());
assert_eq!(engine.stats.total_lowered(), 0);
}
#[test]
fn test_make_functions() {
let sse2 = make_x86_interleaved_access_sse2();
assert_eq!(sse2.isa, InterleaveIsa::SSE2);
let avx2 = make_x86_interleaved_access_avx2();
assert_eq!(avx2.isa, InterleaveIsa::AVX2);
let avx512 = make_x86_interleaved_access_avx512();
assert_eq!(avx512.isa, InterleaveIsa::AVX512F);
}
#[test]
fn test_config_defaults() {
let config = InterleaveConfig::default();
assert_eq!(config.max_stride, 4);
assert_eq!(config.min_members, 2);
assert!(config.enable_stride3);
assert!(config.prefer_interleaved);
}
#[test]
fn test_sse2_vs_avx2_cost() {
let sse2 = make_sse2();
let avx2 = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
let cost_sse2 = sse2.cost_for_group(&g);
let cost_avx2 = avx2.cost_for_group(&g);
assert!(cost_avx2.interleaved_cost <= cost_sse2.interleaved_cost);
}
#[test]
fn test_lower_stride_2_sse2() {
let mut engine = make_sse2();
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::SSE2);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_lower_stride_2_store_sse2() {
let mut engine = make_sse2();
let g = InterleavedAccessGroup::new_store("buf", 2, 4, 4, 16, InterleaveIsa::SSE2);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_lower_stride_2_avx512() {
let mut engine = make_avx512();
let g = InterleavedAccessGroup::new_load("buf", 2, 8, 4, 64, InterleaveIsa::AVX512F);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_lower_stride_4_avx512() {
let mut engine = make_avx512();
let g = InterleavedAccessGroup::new_store("buf", 4, 8, 4, 64, InterleaveIsa::AVX512F);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_lowering_sequence_description() {
let engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(!seq.description.is_empty());
}
#[test]
fn test_unsupported_stride_5() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 5, 2, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(!seq.is_lowered);
assert!(seq.description.contains("unsupported stride"));
}
#[test]
fn test_lower_empty_group_list() {
let mut engine = make_avx2();
let seqs = engine.lower_all();
assert!(seqs.is_empty());
}
#[test]
fn test_compute_sequence_cost_empty() {
let engine = make_avx2();
let cost = engine.compute_sequence_cost(&[], true);
assert_eq!(cost, 0);
}
#[test]
fn test_stride_1_is_supported_but_minimal() {
let engine = make_avx2();
assert!(engine.is_stride_supported(1));
}
#[test]
fn test_rejected_reason_not_profitable() {
let config = InterleaveConfig {
prefer_interleaved: false,
min_speedup: 1000.0, ..Default::default()
};
let mut engine = make_avx2().with_config(config);
let g = InterleavedAccessGroup::new_load("buf", 2, 2, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(!seq.is_lowered);
assert!(seq.description.contains("not profitable"));
}
#[test]
fn test_multiple_registrations_different_stride() {
let mut engine = make_avx2();
engine.register_load("a", 2, 2, 4, 16);
engine.register_load("b", 3, 3, 4, 16);
engine.register_load("c", 4, 4, 8, 32);
engine.register_store("d", 2, 4, 4, 16);
let seqs = engine.lower_all();
assert_eq!(seqs.len(), 4);
assert_eq!(engine.groups_lowered, 4);
}
#[test]
fn test_cost_data_populated() {
let mut engine = make_avx2();
engine.register_load("unique_buf", 2, 2, 4, 16);
engine.lower_all();
assert!(engine.cost_data.contains_key("unique_buf"));
}
#[test]
fn test_shuffle_step_clone() {
let s = ShuffleStep::new("vshufps", "d", vec!["a".into()]).with_imm(0x44);
let s2 = s.clone();
assert_eq!(s.mnemonic, s2.mnemonic);
assert_eq!(s.immediate, s2.immediate);
}
#[test]
fn test_interleave_group_clone() {
let g = InterleavedAccessGroup::new_load("b", 2, 4, 4, 16, InterleaveIsa::AVX2);
let g2 = g.clone();
assert_eq!(g.base_ptr, g2.base_ptr);
assert_eq!(g.stride, g2.stride);
}
#[test]
fn test_lowering_sequence_clone() {
let g = InterleavedAccessGroup::new_load("b", 2, 2, 4, 16, InterleaveIsa::AVX2);
let seq = LoweringSequence {
group: g,
steps: vec![],
estimated_cost: 10,
is_lowered: true,
description: "test".into(),
};
let seq2 = seq.clone();
assert_eq!(seq.estimated_cost, seq2.estimated_cost);
}
#[test]
fn test_stress_many_groups() {
let mut engine = make_avx2();
for i in 0..20 {
engine.register_load(&format!("buf{}", i), 2, 4, 4, 16);
}
let seqs = engine.lower_all();
assert_eq!(seqs.len(), 20);
assert_eq!(engine.groups_lowered, 20);
}
#[test]
fn test_all_isa_levels() {
let levels = [
InterleaveIsa::SSE2,
InterleaveIsa::SSE3,
InterleaveIsa::SSSE3,
InterleaveIsa::SSE41,
InterleaveIsa::SSE42,
InterleaveIsa::AVX,
InterleaveIsa::AVX2,
InterleaveIsa::AVX512F,
InterleaveIsa::AVX512BW,
InterleaveIsa::AVX512VL,
];
for isa in &levels {
let engine = X86InterleavedAccess::new(*isa);
assert_eq!(engine.isa, *isa);
assert!(engine.isa.vector_width() >= 128);
}
}
#[test]
fn test_stride_2_load_uses_unpck_shuffles() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
let has_unpckl = seq.steps.iter().any(|s| s.mnemonic.contains("unpcklps"));
let has_unpckh = seq.steps.iter().any(|s| s.mnemonic.contains("unpckhps"));
assert!(has_unpckl || seq.steps.iter().any(|s| s.mnemonic.contains("unpckl")));
assert!(has_unpckh || seq.steps.iter().any(|s| s.mnemonic.contains("unpckh")));
}
#[test]
fn test_stride_2_f64_load_uses_pd_shuffles() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 2, 8, 32, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
let has_pd = seq.steps.iter().any(|s| s.mnemonic.contains("pd"));
assert!(has_pd || !seq.steps.is_empty());
}
#[test]
fn test_stride_3_load_uses_blend() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 3, 3, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
let load_count = seq
.steps
.iter()
.filter(|s| s.mnemonic.contains("vmovdqu"))
.count();
assert_eq!(load_count, 3);
}
#[test]
fn test_stride_4_load_has_eight_or_more_steps() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 4, 4, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(seq.steps.len() >= 4);
}
#[test]
fn test_lowering_sequence_estimated_cost_positive() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(seq.estimated_cost > 0);
assert!(seq.estimated_cost < u32::MAX);
}
#[test]
fn test_rejected_lowering_cost_is_max() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 5, 2, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(!seq.is_lowered);
assert_eq!(seq.estimated_cost, u32::MAX);
}
#[test]
fn test_rejected_and_accepted_mix() {
let mut engine = make_avx2().with_config(InterleaveConfig {
max_stride: 2,
..Default::default()
});
engine.register_load("ok", 2, 2, 4, 16);
engine.register_load("bad", 4, 4, 4, 16);
let seqs = engine.lower_all();
assert_eq!(seqs.len(), 2);
assert_eq!(engine.groups_lowered, 1);
assert_eq!(engine.groups_rejected, 1);
}
#[test]
fn test_cost_for_stride_3_is_higher_than_stride_2() {
let engine = make_avx2();
let g2 = InterleavedAccessGroup::new_load("a", 2, 2, 4, 16, InterleaveIsa::AVX2);
let g3 = InterleavedAccessGroup::new_load("a", 3, 3, 4, 16, InterleaveIsa::AVX2);
let c2 = engine.cost_for_group(&g2);
let c3 = engine.cost_for_group(&g3);
assert!(c3.shuffle_count >= c2.shuffle_count);
}
#[test]
fn test_interleaved_cost_less_than_scalar_for_stride_2() {
let engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
let cost = engine.cost_for_group(&g);
assert!(cost.interleaved_cost < cost.scalar_cost);
}
#[test]
fn test_quick_cost_consistency() {
let engine = make_avx2();
let qc = engine.quick_cost(2, 4, true);
let g = InterleavedAccessGroup::new_load("tmp", 2, 4, 4, 16, InterleaveIsa::AVX2);
let fc = engine.cost_for_group(&g);
assert_eq!(qc.prefer_interleaved, fc.prefer_interleaved);
}
#[test]
fn test_element_type_i8_lowering() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 16, 1, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
let has_bw = seq.steps.iter().any(|s| s.mnemonic.contains("bw"));
let has_dq = seq.steps.iter().any(|s| s.mnemonic.contains("dq"));
assert!(seq.is_lowered);
}
#[test]
fn test_element_type_i16_lowering() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 8, 2, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_element_type_i64_lowering() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 2, 8, 32, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_interleaved_access_with_alignment_check() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 64, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_interleaved_access_unaligned() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 1, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_disable_interleaved_via_config() {
let config = InterleaveConfig {
prefer_interleaved: false,
..Default::default()
};
let mut engine = make_avx2().with_config(config);
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered); }
#[test]
fn test_high_min_speedup_rejects() {
let config = InterleaveConfig {
min_speedup: 100.0,
..Default::default()
};
let mut engine = make_avx2().with_config(config);
let g = InterleavedAccessGroup::new_load("buf", 2, 2, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(!seq.is_lowered);
}
#[test]
fn test_custom_shuffle_costs() {
let config = InterleaveConfig {
in_lane_shuffle_cost: 10,
cross_lane_shuffle_cost: 50,
..Default::default()
};
let engine = make_avx2().with_config(config);
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
let cost = engine.cost_for_group(&g);
assert!(cost.interleaved_cost > 20);
}
#[test]
fn test_all_strides_1_to_4_with_sse2() {
let mut engine = make_sse2();
for stride in 1..=4u32 {
if stride == 1 || stride > 4 {
continue;
} let g = InterleavedAccessGroup::new_load("buf", stride, 2, 4, 16, InterleaveIsa::SSE2);
let seq = engine.lower_group(&g);
if stride <= engine.config.max_stride && (stride != 3 || engine.config.enable_stride3) {
assert!(
seq.is_lowered,
"stride {} should be lowered on SSE2",
stride
);
}
}
}
#[test]
fn test_cost_prefer_interleaved_heuristic() {
let engine = make_avx2();
for stride in [2u32, 3, 4].iter() {
for members in [2u32, 4].iter() {
let g = InterleavedAccessGroup::new_load(
"x",
*stride,
*members,
4,
16,
InterleaveIsa::AVX2,
);
let cost = engine.cost_for_group(&g);
assert!(cost.interleaved_cost > 0);
assert!(cost.scalar_cost > 0);
assert!(cost.total_instructions > 0);
}
}
}
#[test]
fn test_min_members_config() {
let config = InterleaveConfig {
min_members: 4,
..Default::default()
};
let engine = make_avx2().with_config(config);
let g_small = InterleavedAccessGroup::new_load("buf", 2, 2, 4, 16, InterleaveIsa::AVX2);
let g_ok = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
assert!(!engine.is_candidate(&g_small));
assert!(engine.is_candidate(&g_ok));
}
#[test]
fn test_store_cost_higher_than_load() {
let engine = make_avx2();
let g_load = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
let g_store = InterleavedAccessGroup::new_store("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
let c_load = engine.cost_for_group(&g_load);
let c_store = engine.cost_for_group(&g_store);
assert!(c_store.shuffle_count >= c_load.shuffle_count);
}
#[test]
fn test_avx512_vs_avx2_stride_4() {
let engine_avx2 = make_avx2();
let engine_avx512 = make_avx512();
let g = InterleavedAccessGroup::new_load("buf", 4, 4, 4, 16, InterleaveIsa::AVX2);
let c_avx2 = engine_avx2.cost_for_group(&g);
let c_avx512 = engine_avx512.cost_for_group(&g);
assert!(c_avx512.interleaved_cost <= c_avx2.interleaved_cost);
}
#[test]
fn test_avx512vl_stride_4() {
let engine = X86InterleavedAccess::new(InterleaveIsa::AVX512VL);
let g = InterleavedAccessGroup::new_store("buf", 4, 8, 4, 64, InterleaveIsa::AVX512VL);
let cost = engine.cost_for_group(&g);
assert!(cost.interleaved_cost > 0);
}
#[test]
fn test_avx512bw_stride_2() {
let engine = X86InterleavedAccess::new(InterleaveIsa::AVX512BW);
let g = InterleavedAccessGroup::new_load("buf", 2, 16, 1, 64, InterleaveIsa::AVX512BW);
let cost = engine.cost_for_group(&g);
assert!(cost.interleaved_cost > 0);
assert!(cost.prefer_interleaved);
}
#[test]
fn test_lowering_with_sse3_isa() {
let mut engine = X86InterleavedAccess::new(InterleaveIsa::SSE3);
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::SSE3);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_lowering_with_ssse3_isa() {
let mut engine = X86InterleavedAccess::new(InterleaveIsa::SSSE3);
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::SSSE3);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_lowering_with_sse41_isa() {
let mut engine = X86InterleavedAccess::new(InterleaveIsa::SSE41);
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::SSE41);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_lowering_with_avx_isa() {
let mut engine = X86InterleavedAccess::new(InterleaveIsa::AVX);
let g = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX);
let seq = engine.lower_group(&g);
assert!(seq.is_lowered);
}
#[test]
fn test_contextual_cost_vs_isa() {
let sse2 = make_sse2();
let avx2 = make_avx2();
let avx512 = make_avx512();
let g = InterleavedAccessGroup::new_load("buf", 4, 4, 4, 16, InterleaveIsa::AVX2);
let c1 = sse2.cost_for_group(&g);
let c2 = avx2.cost_for_group(&g);
let c3 = avx512.cost_for_group(&g);
assert!(c2.interleaved_cost <= c1.interleaved_cost);
assert!(c3.interleaved_cost <= c1.interleaved_cost);
}
#[test]
fn test_sequence_description_non_empty() {
let mut engine = make_avx2();
engine.register_load("buf", 2, 4, 4, 16);
let seqs = engine.lower_all();
for seq in &seqs {
assert!(!seq.description.is_empty());
}
}
#[test]
fn test_group_equality() {
let g1 = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
let g2 = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
assert_eq!(g1, g2);
}
#[test]
fn test_group_inequality_different_stride() {
let g1 = InterleavedAccessGroup::new_load("buf", 2, 4, 4, 16, InterleaveIsa::AVX2);
let g2 = InterleavedAccessGroup::new_load("buf", 3, 4, 4, 16, InterleaveIsa::AVX2);
assert_ne!(g1, g2);
}
#[test]
fn test_shuffle_step_equality() {
let s1 = ShuffleStep::new("vshufps", "d", vec!["a".into()]).with_imm(0x44);
let s2 = ShuffleStep::new("vshufps", "d", vec!["a".into()]).with_imm(0x44);
assert_eq!(s1, s2);
}
#[test]
fn test_shuffle_step_inequality() {
let s1 = ShuffleStep::new("a", "d", vec!["x".into()]);
let s2 = ShuffleStep::new("b", "d", vec!["x".into()]);
assert_ne!(s1, s2);
}
#[test]
fn test_element_type_all_variants() {
let types = [
InterleaveElementType::I8,
InterleaveElementType::I16,
InterleaveElementType::I32,
InterleaveElementType::I64,
InterleaveElementType::F32,
InterleaveElementType::F64,
];
for t in &types {
assert!(t.bits() > 0);
assert!(t.size_bytes() > 0);
let s = format!("{}", t);
assert!(!s.is_empty());
}
}
#[test]
fn test_isa_display_all_variants() {
let isas = [
InterleaveIsa::SSE2,
InterleaveIsa::SSE3,
InterleaveIsa::SSSE3,
InterleaveIsa::SSE41,
InterleaveIsa::SSE42,
InterleaveIsa::AVX,
InterleaveIsa::AVX2,
InterleaveIsa::AVX512F,
InterleaveIsa::AVX512BW,
InterleaveIsa::AVX512VL,
];
for isa in &isas {
let s = format!("{}", isa);
assert!(!s.is_empty());
}
}
#[test]
fn test_stats_zero_initially() {
let stats = InterleaveStats::default();
assert_eq!(stats.total_lowered(), 0);
}
#[test]
fn test_stats_rejected_counting() {
let mut engine = make_avx2().with_config(InterleaveConfig {
max_stride: 2,
..Default::default()
});
engine.register_load("a", 2, 2, 4, 16);
engine.register_load("b", 3, 3, 4, 16);
engine.register_load("c", 4, 4, 4, 16);
engine.lower_all();
assert_eq!(engine.stats.rejected_unsupported, 2);
assert_eq!(engine.stats.total_lowered(), 1);
}
#[test]
fn test_group_total_bytes_computation() {
let g = InterleavedAccessGroup::new_load("p", 3, 4, 8, 64, InterleaveIsa::AVX512F);
assert_eq!(g.bytes_per_member(), 32); assert_eq!(g.total_bytes(), 96); }
#[test]
fn test_no_panic_on_zero_stride() {
let engine = make_avx2();
assert!(!engine.is_stride_supported(0));
}
#[test]
fn test_max_stride_boundary() {
let engine = make_avx2().with_config(InterleaveConfig {
max_stride: 8,
..Default::default()
});
assert!(engine.is_stride_supported(8));
assert!(!engine.is_stride_supported(9));
}
#[test]
fn test_stride_3_disabled_via_config_unsupported() {
let engine = make_avx2().with_config(InterleaveConfig {
enable_stride3: false,
..Default::default()
});
assert!(!engine.is_stride_supported(3));
}
#[test]
fn test_sse2_isa_doesnt_have_three_operand() {
assert!(!InterleaveIsa::SSE2.has_3_operand_shuffle());
assert!(!InterleaveIsa::SSE3.has_3_operand_shuffle());
assert!(!InterleaveIsa::SSSE3.has_3_operand_shuffle());
assert!(!InterleaveIsa::SSE41.has_3_operand_shuffle());
assert!(!InterleaveIsa::SSE42.has_3_operand_shuffle());
}
#[test]
fn test_avx_and_newer_have_three_operand() {
assert!(InterleaveIsa::AVX.has_3_operand_shuffle());
assert!(InterleaveIsa::AVX2.has_3_operand_shuffle());
assert!(InterleaveIsa::AVX512F.has_3_operand_shuffle());
assert!(InterleaveIsa::AVX512VL.has_3_operand_shuffle());
}
#[test]
fn test_cross_lane_permute_requires_avx2_or_newer() {
assert!(!InterleaveIsa::AVX.has_cross_lane_permute());
assert!(InterleaveIsa::AVX2.has_cross_lane_permute());
assert!(InterleaveIsa::AVX512F.has_cross_lane_permute());
}
#[test]
fn test_cost_mirrors_isa_capabilities() {
let sse = make_sse2();
let avx2 = make_avx2();
let g = InterleavedAccessGroup::new_load("a", 4, 4, 4, 16, InterleaveIsa::AVX2);
let c_sse = sse.cost_for_group(&g);
let c_avx2 = avx2.cost_for_group(&g);
assert!(c_avx2.shuffle_count <= c_sse.shuffle_count + 4);
}
#[test]
fn test_unsupported_group_returns_empty_steps() {
let mut engine = make_avx2();
let g = InterleavedAccessGroup::new_load("buf", 5, 2, 4, 16, InterleaveIsa::AVX2);
let seq = engine.lower_group(&g);
assert!(!seq.is_lowered);
assert!(seq.steps.is_empty());
}
}