use crate::codegen::{MachineBasicBlock, MachineFunction, MachineInstr, MachineOperand};
use crate::x86::x86_instr_info::{X86InstrInfo, X86Opcode};
use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, Clone, Default)]
pub struct CombinerStats {
pub add_add_to_lea: usize,
pub shl_add_to_lea: usize,
pub xor_lea_to_lea: usize,
pub xor_zero_idiom: usize,
pub inc_dec_to_add_sub: usize,
pub mov_op_to_mem_op: usize,
pub redundant_mov_elim: usize,
pub lea_nop_elim: usize,
pub lea_to_mov: usize,
pub shift_shift_combine: usize,
pub cmov_same_to_mov: usize,
pub and_or_same_to_nop: usize,
pub test_cmp_combine: usize,
pub load_store_memcpy: usize,
pub adjacent_load_store: usize,
pub store_load_forward: usize,
pub sse_horizontal: usize,
pub fma_formation: usize,
pub vperm_combine: usize,
pub broadcast_op: usize,
pub cmp_setcc_combine: usize,
pub push_pop_elim: usize,
pub cmp_zero_to_test: usize,
pub imm_fold_into_cmp: usize,
pub redundant_flag_use: usize,
pub total_combined: usize,
}
impl CombinerStats {
pub fn merge(&mut self, other: &CombinerStats) {
self.add_add_to_lea += other.add_add_to_lea;
self.shl_add_to_lea += other.shl_add_to_lea;
self.xor_lea_to_lea += other.xor_lea_to_lea;
self.xor_zero_idiom += other.xor_zero_idiom;
self.inc_dec_to_add_sub += other.inc_dec_to_add_sub;
self.mov_op_to_mem_op += other.mov_op_to_mem_op;
self.redundant_mov_elim += other.redundant_mov_elim;
self.lea_nop_elim += other.lea_nop_elim;
self.lea_to_mov += other.lea_to_mov;
self.shift_shift_combine += other.shift_shift_combine;
self.cmov_same_to_mov += other.cmov_same_to_mov;
self.and_or_same_to_nop += other.and_or_same_to_nop;
self.test_cmp_combine += other.test_cmp_combine;
self.load_store_memcpy += other.load_store_memcpy;
self.adjacent_load_store += other.adjacent_load_store;
self.store_load_forward += other.store_load_forward;
self.sse_horizontal += other.sse_horizontal;
self.fma_formation += other.fma_formation;
self.vperm_combine += other.vperm_combine;
self.broadcast_op += other.broadcast_op;
self.cmp_setcc_combine += other.cmp_setcc_combine;
self.push_pop_elim += other.push_pop_elim;
self.cmp_zero_to_test += other.cmp_zero_to_test;
self.imm_fold_into_cmp += other.imm_fold_into_cmp;
self.redundant_flag_use += other.redundant_flag_use;
self.total_combined += other.total_combined;
}
pub fn made_progress(&self) -> bool {
self.total_combined > 0
}
pub fn summary(&self) -> String {
format!(
"X86MachineCombine: total={}, lea={}, fma={}, mov_elim={}, shift={}, xor_idiom={}, memop={}, pushpop={}",
self.total_combined, self.add_add_to_lea + self.shl_add_to_lea,
self.fma_formation, self.redundant_mov_elim, self.shift_shift_combine,
self.xor_zero_idiom, self.mov_op_to_mem_op, self.push_pop_elim,
)
}
}
fn mi_opcode(mi: &MachineInstr) -> u32 {
mi.opcode
}
fn is_opcode(mi: &MachineInstr, op: X86Opcode) -> bool {
mi.opcode == op as u32
}
fn mi_def(mi: &MachineInstr) -> Option<u32> {
mi.def
}
fn mi_reg_op(mi: &MachineInstr, idx: usize) -> Option<u32> {
mi.operands.get(idx).and_then(|op| match op {
MachineOperand::Reg(r) | MachineOperand::PhysReg(r) => Some(*r),
_ => None,
})
}
fn mi_imm_op(mi: &MachineInstr, idx: usize) -> Option<i64> {
mi.operands.get(idx).and_then(|op| match op {
MachineOperand::Imm(v) => Some(*v),
_ => None,
})
}
fn mi_label_op(mi: &MachineInstr, idx: usize) -> Option<String> {
mi.operands.get(idx).and_then(|op| match op {
MachineOperand::Label(s) | MachineOperand::Global(s) => Some(s.clone()),
_ => None,
})
}
fn same_reg(a: u32, b: u32) -> bool {
a == b
}
fn is_nop_like(mi: &MachineInstr) -> bool {
mi.opcode == X86Opcode::NOP as u32
|| mi.opcode == X86Opcode::NOP1 as u32
|| mi.opcode == X86Opcode::NOP2 as u32
|| mi.opcode == X86Opcode::NOP3 as u32
|| mi.opcode == X86Opcode::NOP4 as u32
|| mi.opcode == X86Opcode::NOP5 as u32
}
pub struct X86MachineCombine {
pub stats: CombinerStats,
pub enable_fma_formation: bool,
pub enable_horizontal_ops: bool,
pub enable_load_store_combine: bool,
pub enable_vperm_combine: bool,
pub enable_store_load_forward: bool,
pub enable_push_pop_elim: bool,
pub enable_cmp_optimization: bool,
pub max_iterations: usize,
dead_regs: HashSet<u32>,
}
impl X86MachineCombine {
pub fn new() -> Self {
Self {
stats: CombinerStats::default(),
enable_fma_formation: true,
enable_horizontal_ops: true,
enable_load_store_combine: true,
enable_vperm_combine: true,
enable_store_load_forward: true,
enable_push_pop_elim: true,
enable_cmp_optimization: true,
max_iterations: 8,
dead_regs: HashSet::new(),
}
}
pub fn run(&mut self, mf: &mut MachineFunction, _instr_info: &X86InstrInfo) -> bool {
let mut changed = false;
for block in &mut mf.blocks {
let mut iter = 0;
loop {
let bc = self.combine_block(block);
if bc {
changed = true;
}
iter += 1;
if !bc || iter >= self.max_iterations {
break;
}
}
}
changed
}
pub fn combine_block(&mut self, block: &mut MachineBasicBlock) -> bool {
let mut changed = false;
let n = block.instructions.len();
self.build_dead_regs(block);
let mut i = 0;
while i < n {
if i + 1 < n {
let (left, right) = block.instructions.split_at_mut(i + 1);
let a = &mut left[i];
let b = &mut right[0];
if self.try_add_add_to_lea(a, b) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_shl_add_to_lea(a, b) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_xor_add_to_lea(a, b) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_mov_op_to_mem_op(a, b) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_shift_shift_combine(a, b) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_fma_formation(a, b) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_vperm_combine(a, b) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_broadcast_op(a, b) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_store_load_forward(a, b) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_imm_fold_into_cmp(a, b) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_cmp_setcc_combine(a, b) {
changed = true;
self.stats.total_combined += 1;
}
let a = &block.instructions[i];
let b = &block.instructions[i + 1];
if self.try_test_cmp_combine(a, b) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_cmp_zero_to_test(a, b) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_redundant_flag_use(a, b) {
changed = true;
self.stats.total_combined += 1;
}
}
{
let inst = &mut block.instructions[i];
if self.try_xor_zero_idiom(inst) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_inc_dec_to_add_sub(inst) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_redundant_mov(inst) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_lea_nop(inst) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_lea_to_mov(inst) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_cmov_same_src(inst) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_and_or_same_reg(inst) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_horizontal_op(inst) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_push_pop_elim(inst) {
changed = true;
self.stats.total_combined += 1;
}
}
if i + 2 < n {
let a = &block.instructions[i];
let b = &block.instructions[i + 1];
let c = &block.instructions[i + 2];
if self.try_load_store_memcpy(a, b, c) {
changed = true;
self.stats.total_combined += 1;
}
if self.try_adjacent_load_store(a, b, c) {
changed = true;
self.stats.total_combined += 1;
}
}
i += 1;
}
changed
}
fn try_add_add_to_lea(&mut self, a: &mut MachineInstr, b: &mut MachineInstr) -> bool {
if !is_opcode(a, X86Opcode::ADD) || !is_opcode(b, X86Opcode::ADD) {
return false;
}
let a_dst = match mi_def(a) {
Some(r) => r,
None => return false,
};
let a_src = match mi_reg_op(a, 1) {
Some(r) => r,
None => return false,
};
let b_src1 = match mi_reg_op(b, 0) {
Some(r) => r,
None => return false,
};
let b_src2 = match mi_reg_op(b, 1) {
Some(r) => r,
None => return false,
};
if same_reg(b_src2, a_dst) && !same_reg(b_src1, a_dst) {
let b_def = mi_def(b).unwrap_or(0);
b.opcode = X86Opcode::LEA as u32;
b.operands.clear();
b.operands.push(MachineOperand::Reg(b_src1));
b.operands.push(MachineOperand::Reg(a_src));
b.def = Some(b_def);
self.stats.add_add_to_lea += 1;
return true;
}
false
}
fn try_shl_add_to_lea(&mut self, a: &mut MachineInstr, b: &mut MachineInstr) -> bool {
if !is_opcode(a, X86Opcode::SHL) || !is_opcode(b, X86Opcode::ADD) {
return false;
}
let shift = match mi_imm_op(a, 1) {
Some(v) if (1..=3).contains(&v) => v as u8,
_ => return false,
};
let a_dst = match mi_def(a) {
Some(r) => r,
None => return false,
};
let a_src = match mi_reg_op(a, 0) {
Some(r) => r,
None => return false,
};
let b_src1 = match mi_reg_op(b, 0) {
Some(r) => r,
None => return false,
};
let b_src2 = match mi_reg_op(b, 1) {
Some(r) => r,
None => return false,
};
if same_reg(b_src2, a_dst) {
let b_def = mi_def(b).unwrap_or(0);
b.opcode = X86Opcode::LEA as u32;
b.operands.clear();
b.operands.push(MachineOperand::Reg(b_src1));
b.operands.push(MachineOperand::Reg(a_src));
b.operands.push(MachineOperand::Imm(shift as i64));
b.def = Some(b_def);
self.stats.shl_add_to_lea += 1;
return true;
}
false
}
fn try_xor_add_to_lea(&mut self, a: &mut MachineInstr, b: &mut MachineInstr) -> bool {
if !is_opcode(a, X86Opcode::XOR) || !is_opcode(b, X86Opcode::ADD) {
return false;
}
let a_dst = match mi_def(a) {
Some(r) => r,
None => return false,
};
if a.operands.len() >= 2 && a.operands[0] == a.operands[1] {
let b_src1 = match mi_reg_op(b, 0) {
Some(r) => r,
None => return false,
};
let b_src2 = match mi_reg_op(b, 1) {
Some(r) => r,
None => return false,
};
if same_reg(b_src2, a_dst) {
let b_def = mi_def(b).unwrap_or(0);
b.opcode = X86Opcode::LEA as u32;
b.operands.clear();
b.operands.push(MachineOperand::Reg(b_src1));
b.operands.push(MachineOperand::Reg(b_src1));
b.def = Some(b_def);
self.stats.xor_lea_to_lea += 1;
return true;
}
}
false
}
fn try_xor_zero_idiom(&mut self, mi: &mut MachineInstr) -> bool {
if !is_opcode(mi, X86Opcode::XOR) {
return false;
}
if mi.operands.len() >= 2 && mi.operands[0] == mi.operands[1] {
self.stats.xor_zero_idiom += 1;
return true;
}
false
}
fn try_inc_dec_to_add_sub(&mut self, mi: &mut MachineInstr) -> bool {
if is_opcode(mi, X86Opcode::INC) {
mi.opcode = X86Opcode::ADD as u32;
mi.operands.push(MachineOperand::Imm(1));
self.stats.inc_dec_to_add_sub += 1;
return true;
}
if is_opcode(mi, X86Opcode::DEC) {
mi.opcode = X86Opcode::SUB as u32;
mi.operands.push(MachineOperand::Imm(1));
self.stats.inc_dec_to_add_sub += 1;
return true;
}
false
}
fn try_mov_op_to_mem_op(&mut self, a: &mut MachineInstr, b: &mut MachineInstr) -> bool {
if !is_opcode(a, X86Opcode::MOV) {
return false;
}
let a_def = match mi_def(a) {
Some(r) => r,
None => return false,
};
if !self.dead_regs.contains(&a_def) {
return false;
}
let uses_a = b
.operands
.iter()
.any(|op| matches!(op, MachineOperand::Reg(r) if *r == a_def));
if !uses_a {
return false;
}
let mem = a
.operands
.iter()
.find(|op| matches!(op, MachineOperand::Label(_) | MachineOperand::Global(_)))
.cloned();
if let Some(mem_op) = mem {
for op in &mut b.operands {
if *op == MachineOperand::Reg(a_def) {
*op = mem_op.clone();
break;
}
}
a.opcode = X86Opcode::NOP as u32;
a.operands.clear();
a.def = None;
self.stats.mov_op_to_mem_op += 1;
return true;
}
false
}
fn try_redundant_mov(&mut self, mi: &mut MachineInstr) -> bool {
if !is_opcode(mi, X86Opcode::MOV) {
return false;
}
if let (Some(dst), Some(src)) = (mi_def(mi), mi_reg_op(mi, 0)) {
if same_reg(dst, src) {
mi.opcode = X86Opcode::NOP as u32;
mi.operands.clear();
mi.def = None;
self.stats.redundant_mov_elim += 1;
return true;
}
}
false
}
fn try_lea_nop(&mut self, mi: &mut MachineInstr) -> bool {
if !is_opcode(mi, X86Opcode::LEA) {
return false;
}
let dst = match mi_reg_op(mi, 0) {
Some(r) => r,
None => return false,
};
let base = match mi_reg_op(mi, 1) {
Some(r) => r,
None => return false,
};
let disp = mi_imm_op(mi, 2).unwrap_or(0);
let scale = mi_imm_op(mi, 3).unwrap_or(0);
if same_reg(dst, base) && disp == 0 && scale == 0 {
mi.opcode = X86Opcode::NOP as u32;
mi.operands.clear();
mi.def = None;
self.stats.lea_nop_elim += 1;
return true;
}
false
}
fn try_lea_to_mov(&mut self, mi: &mut MachineInstr) -> bool {
if !is_opcode(mi, X86Opcode::LEA) {
return false;
}
let dst = match mi_reg_op(mi, 0) {
Some(r) => r,
None => return false,
};
let base = match mi_reg_op(mi, 1) {
Some(r) => r,
None => return false,
};
let disp = mi_imm_op(mi, 2).unwrap_or(0);
let index = mi_reg_op(mi, 3);
if index.is_none() && disp == 0 {
if !same_reg(dst, base) {
mi.opcode = X86Opcode::MOV as u32;
mi.operands = vec![MachineOperand::Reg(base)];
self.stats.lea_to_mov += 1;
return true;
}
}
false
}
fn try_shift_shift_combine(&mut self, a: &mut MachineInstr, b: &mut MachineInstr) -> bool {
let a_op = mi_opcode(a);
let b_op = mi_opcode(b);
if a_op != b_op {
return false;
}
let is_shift = matches!(a_op,
o if o == X86Opcode::SHL as u32 || o == X86Opcode::SHR as u32 || o == X86Opcode::SAR as u32
);
if !is_shift {
return false;
}
let a_dst = match mi_def(a) {
Some(r) => r,
None => return false,
};
let b_src = match mi_reg_op(b, 0) {
Some(r) => r,
None => return false,
};
if !same_reg(a_dst, b_src) {
return false;
}
let a_amt = mi_imm_op(a, 1).unwrap_or(0);
let b_amt = mi_imm_op(b, 1).unwrap_or(0);
let combined = a_amt + b_amt;
if combined > 63 {
return false;
}
b.operands[1] = MachineOperand::Imm(combined);
a.opcode = X86Opcode::NOP as u32;
a.operands.clear();
a.def = None;
self.stats.shift_shift_combine += 1;
true
}
fn try_cmov_same_src(&mut self, mi: &mut MachineInstr) -> bool {
let op = mi_opcode(mi);
let is_cmov = matches!(op,
o if o == X86Opcode::CMOVO as u32 || o == X86Opcode::CMOVNO as u32
|| o == X86Opcode::CMOVB as u32 || o == X86Opcode::CMOVAE as u32
|| o == X86Opcode::CMOVE as u32 || o == X86Opcode::CMOVNE as u32
|| o == X86Opcode::CMOVBE as u32 || o == X86Opcode::CMOVA as u32
|| o == X86Opcode::CMOVS as u32 || o == X86Opcode::CMOVNS as u32
|| o == X86Opcode::CMOVP as u32 || o == X86Opcode::CMOVNP as u32
|| o == X86Opcode::CMOVL as u32 || o == X86Opcode::CMOVGE as u32
|| o == X86Opcode::CMOVLE as u32 || o == X86Opcode::CMOVG as u32
);
if !is_cmov {
return false;
}
let dst = match mi_def(mi) {
Some(r) => r,
None => return false,
};
let src = match mi_reg_op(mi, 1) {
Some(r) => r,
None => return false,
};
if same_reg(dst, src) {
mi.opcode = X86Opcode::NOP as u32;
mi.operands.clear();
mi.def = None;
self.stats.cmov_same_to_mov += 1;
return true;
}
false
}
fn try_and_or_same_reg(&mut self, mi: &mut MachineInstr) -> bool {
if !is_opcode(mi, X86Opcode::AND) && !is_opcode(mi, X86Opcode::OR) {
return false;
}
if mi.operands.len() >= 2 && mi.operands[0] == mi.operands[1] {
mi.opcode = X86Opcode::NOP as u32;
mi.operands.clear();
mi.def = None;
self.stats.and_or_same_to_nop += 1;
return true;
}
false
}
fn try_test_cmp_combine(&self, a: &MachineInstr, b: &MachineInstr) -> bool {
if !is_opcode(a, X86Opcode::TEST) || !is_opcode(b, X86Opcode::CMP) {
return false;
}
if a.operands.len() >= 2 && a.operands[0] == a.operands[1] {
if b.operands.len() >= 2 {
if let MachineOperand::Imm(0) = b.operands[1] {
if a.operands[0] == b.operands[0] {
return true;
}
}
}
}
false
}
fn try_cmp_zero_to_test(&self, a: &MachineInstr, _b: &MachineInstr) -> bool {
if !is_opcode(a, X86Opcode::CMP) {
return false;
}
if a.operands.len() >= 2 {
if let MachineOperand::Imm(0) = a.operands[1] {
return true;
}
}
false
}
fn try_store_load_forward(&mut self, a: &mut MachineInstr, b: &mut MachineInstr) -> bool {
if !self.enable_store_load_forward {
return false;
}
let a_is_store = is_opcode(a, X86Opcode::MOV)
&& a.operands.first().map_or(false, |o| {
matches!(o, MachineOperand::Label(_) | MachineOperand::Global(_))
});
let b_is_load = is_opcode(b, X86Opcode::MOV)
&& b.operands.len() >= 2
&& b.operands[1..]
.iter()
.any(|o| matches!(o, MachineOperand::Label(_) | MachineOperand::Global(_)));
if !a_is_store || !b_is_load {
return false;
}
let store_addr = a.operands.first().cloned();
let load_addr = b
.operands
.iter()
.find(|o| matches!(o, MachineOperand::Label(_) | MachineOperand::Global(_)))
.cloned();
if store_addr == load_addr {
let src_reg = a.operands.get(1).cloned();
if let Some(src) = src_reg {
let b_def = mi_def(b).unwrap_or(0);
b.opcode = X86Opcode::MOV as u32;
b.operands = vec![src];
b.def = Some(b_def);
self.stats.store_load_forward += 1;
return true;
}
}
false
}
fn try_imm_fold_into_cmp(&mut self, a: &mut MachineInstr, b: &mut MachineInstr) -> bool {
if !self.enable_cmp_optimization {
return false;
}
if !is_opcode(a, X86Opcode::MOV) || !is_opcode(b, X86Opcode::CMP) {
return false;
}
let a_def = match mi_def(a) {
Some(r) => r,
None => return false,
};
if !self.dead_regs.contains(&a_def) {
return false;
}
let imm = match mi_imm_op(a, 0) {
Some(v) => v,
None => return false,
};
let cmp_uses_a = b
.operands
.iter()
.any(|op| matches!(op, MachineOperand::Reg(r) if *r == a_def));
if !cmp_uses_a {
return false;
}
for op in &mut b.operands {
if *op == MachineOperand::Reg(a_def) {
*op = MachineOperand::Imm(imm);
break;
}
}
a.opcode = X86Opcode::NOP as u32;
a.operands.clear();
a.def = None;
self.stats.imm_fold_into_cmp += 1;
true
}
fn try_cmp_setcc_combine(&mut self, a: &mut MachineInstr, b: &mut MachineInstr) -> bool {
if !self.enable_cmp_optimization {
return false;
}
if !is_opcode(a, X86Opcode::CMP) {
return false;
}
let is_setcc = matches!(mi_opcode(b),
o if o == X86Opcode::SETE as u32 || o == X86Opcode::SETNE as u32
|| o == X86Opcode::SETB as u32 || o == X86Opcode::SETAE as u32
|| o == X86Opcode::SETL as u32 || o == X86Opcode::SETG as u32
|| o == X86Opcode::SETLE as u32 || o == X86Opcode::SETGE as u32
);
if !is_setcc {
return false;
}
if a.operands.len() >= 2 {
if let MachineOperand::Imm(0) = a.operands[1] {
a.opcode = X86Opcode::TEST as u32;
a.operands[1] = a.operands[0].clone();
self.stats.cmp_setcc_combine += 1;
return true;
}
}
false
}
fn try_redundant_flag_use(&self, _a: &MachineInstr, _b: &MachineInstr) -> bool {
false
}
fn try_push_pop_elim(&mut self, mi: &mut MachineInstr) -> bool {
if !self.enable_push_pop_elim {
return false;
}
false
}
fn try_load_store_memcpy(
&mut self,
a: &MachineInstr,
_b: &MachineInstr,
c: &MachineInstr,
) -> bool {
if !is_opcode(a, X86Opcode::MOV) || !is_opcode(c, X86Opcode::MOV) {
return false;
}
let load_def = match mi_def(a) {
Some(r) => r,
None => return false,
};
let store_src = match mi_reg_op(c, 0) {
Some(r) => r,
None => return false,
};
if same_reg(load_def, store_src) {
self.stats.load_store_memcpy += 1;
return true;
}
false
}
fn try_adjacent_load_store(
&mut self,
a: &MachineInstr,
b: &MachineInstr,
_c: &MachineInstr,
) -> bool {
if !is_opcode(a, X86Opcode::MOVZX) || !is_opcode(b, X86Opcode::MOVZX) {
return false;
}
self.stats.adjacent_load_store += 1;
false
}
fn try_horizontal_op(&mut self, mi: &mut MachineInstr) -> bool {
if !self.enable_horizontal_ops {
return false;
}
let op = mi_opcode(mi);
let is_hadd = op == X86Opcode::HADDPS as u32 || op == X86Opcode::HADDPD as u32;
let is_hsub = op == X86Opcode::HSUBPS as u32 || op == X86Opcode::HSUBPD as u32;
if is_hadd || is_hsub {
self.stats.sse_horizontal += 1;
return true;
}
false
}
fn try_fma_formation(&mut self, a: &mut MachineInstr, b: &mut MachineInstr) -> bool {
if !self.enable_fma_formation {
return false;
}
let a_is_mul = matches!(mi_opcode(a),
o if o == X86Opcode::VMULPS as u32 || o == X86Opcode::VMULPD as u32
|| o == X86Opcode::MULPS as u32 || o == X86Opcode::MULPD as u32
|| o == X86Opcode::MULSS as u32 || o == X86Opcode::MULSD as u32
|| o == X86Opcode::VMULSS as u32 || o == X86Opcode::VMULSD as u32
);
let b_is_add = matches!(mi_opcode(b),
o if o == X86Opcode::VADDPS as u32 || o == X86Opcode::VADDPD as u32
|| o == X86Opcode::ADDPS as u32 || o == X86Opcode::ADDPD as u32
|| o == X86Opcode::ADDSS as u32 || o == X86Opcode::ADDSD as u32
|| o == X86Opcode::VADDSS as u32 || o == X86Opcode::VADDSD as u32
);
if !a_is_mul || !b_is_add {
return false;
}
let mul_def = match mi_def(a) {
Some(r) => r,
None => return false,
};
let mul_used = b
.operands
.iter()
.any(|op| matches!(op, MachineOperand::Reg(r) if *r == mul_def));
if !mul_used {
return false;
}
let fma_op = match mi_opcode(a) {
o if o == X86Opcode::VMULPS as u32 || o == X86Opcode::MULPS as u32 => {
X86Opcode::VFMADD213PS as u32
}
o if o == X86Opcode::VMULPD as u32 || o == X86Opcode::MULPD as u32 => {
X86Opcode::VFMADD213PD as u32
}
o if o == X86Opcode::VMULSS as u32 || o == X86Opcode::MULSS as u32 => {
X86Opcode::VFMADD213SS as u32
}
_ => X86Opcode::VFMADD213SD as u32,
};
let mul_src1 = mi_reg_op(a, 0).unwrap_or(0);
let mul_src2 = mi_reg_op(a, 1).unwrap_or(0);
let b_def = mi_def(b).unwrap_or(0);
let other = b
.operands
.iter()
.find(|op| !matches!(op, MachineOperand::Reg(r) if *r == mul_def))
.cloned()
.unwrap_or(MachineOperand::Reg(0));
b.opcode = fma_op;
b.operands = vec![
other,
MachineOperand::Reg(mul_src1),
MachineOperand::Reg(mul_src2),
];
b.def = Some(b_def);
a.opcode = X86Opcode::NOP as u32;
a.operands.clear();
a.def = None;
self.stats.fma_formation += 1;
true
}
fn try_vperm_combine(&mut self, a: &mut MachineInstr, b: &mut MachineInstr) -> bool {
if !self.enable_vperm_combine {
return false;
}
if mi_opcode(a) != X86Opcode::VPERMILPS as u32
|| mi_opcode(b) != X86Opcode::VPERMILPS as u32
{
return false;
}
let a_def = match mi_def(a) {
Some(r) => r,
None => return false,
};
let b_src = match mi_reg_op(b, 1) {
Some(r) => r,
None => return false,
};
if !same_reg(a_def, b_src) {
return false;
}
let a_imm = mi_imm_op(a, 2).unwrap_or(0) as u8;
let b_imm = mi_imm_op(b, 2).unwrap_or(0) as u8;
let combined = compose_shuffle_imm(a_imm, b_imm);
let a_src = mi_reg_op(a, 1).unwrap_or(0);
b.operands[1] = MachineOperand::Reg(a_src);
b.operands[2] = MachineOperand::Imm(combined as i64);
a.opcode = X86Opcode::NOP as u32;
a.operands.clear();
a.def = None;
self.stats.vperm_combine += 1;
true
}
fn try_broadcast_op(&mut self, a: &mut MachineInstr, b: &mut MachineInstr) -> bool {
let a_is_bc = mi_opcode(a) == X86Opcode::VBROADCASTSS as u32
|| mi_opcode(a) == X86Opcode::VBROADCASTSD as u32;
if !a_is_bc {
return false;
}
let b_is_binop = matches!(mi_opcode(b),
o if o == X86Opcode::VADDPS as u32 || o == X86Opcode::VADDPD as u32
|| o == X86Opcode::VMULPS as u32 || o == X86Opcode::VMULPD as u32
|| o == X86Opcode::VSUBPS as u32 || o == X86Opcode::VSUBPD as u32
);
if !b_is_binop {
return false;
}
let bc_def = match mi_def(a) {
Some(r) => r,
None => return false,
};
let bc_used = b
.operands
.iter()
.any(|op| matches!(op, MachineOperand::Reg(r) if *r == bc_def));
if bc_used {
self.stats.broadcast_op += 1;
}
false }
fn build_dead_regs(&mut self, block: &MachineBasicBlock) {
let mut last_def: HashMap<u32, usize> = HashMap::new();
let n = block.instructions.len();
for (i, inst) in block.instructions.iter().enumerate() {
for op in &inst.operands {
if let MachineOperand::Reg(r) = op {
last_def.remove(r);
}
if let MachineOperand::PhysReg(r) = op {
last_def.remove(r);
}
}
if let Some(def) = inst.def {
last_def.insert(def, i);
}
}
self.dead_regs.clear();
for (reg, _) in &last_def {
self.dead_regs.insert(*reg);
}
for inst in &block.instructions {
if inst.opcode == X86Opcode::RET as u32 || inst.opcode == X86Opcode::JMP as u32 {
for op in &inst.operands {
if let MachineOperand::Reg(r) = op {
self.dead_regs.remove(r);
}
}
}
}
}
pub fn clear(&mut self) {
self.stats = CombinerStats::default();
self.dead_regs.clear();
}
}
impl Default for X86MachineCombine {
fn default() -> Self {
Self::new()
}
}
fn compose_shuffle_imm(first: u8, second: u8) -> u8 {
let mut result: u8 = 0;
for i in 0..4u8 {
let first_lane = (first >> (2 * i)) & 0x3;
let second_lane = (second >> (2 * first_lane)) & 0x3;
result |= second_lane << (2 * i);
}
result
}
pub fn make_x86_machine_combine() -> X86MachineCombine {
X86MachineCombine::new()
}
pub fn make_x86_machine_combine_aggressive() -> X86MachineCombine {
X86MachineCombine::new()
}
#[cfg(test)]
mod tests {
use super::*;
fn imm(v: i64) -> MachineOperand {
MachineOperand::Imm(v)
}
fn reg(r: u32) -> MachineOperand {
MachineOperand::Reg(r)
}
fn label(s: &str) -> MachineOperand {
MachineOperand::Label(s.to_string())
}
fn mi(opcode: u32, ops: Vec<MachineOperand>, def: u32) -> MachineInstr {
let mut i = MachineInstr::new(opcode);
i.operands = ops;
i.def = Some(def);
i
}
fn mi_nodef(opcode: u32, ops: Vec<MachineOperand>) -> MachineInstr {
let mut i = MachineInstr::new(opcode);
i.operands = ops;
i
}
#[test]
fn test_xor_zero_idiom() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::XOR as u32, vec![reg(1), reg(1)], 1);
assert!(c.try_xor_zero_idiom(&mut inst));
assert_eq!(c.stats.xor_zero_idiom, 1);
}
#[test]
fn test_xor_not_self() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::XOR as u32, vec![reg(1), reg(2)], 1);
assert!(!c.try_xor_zero_idiom(&mut inst));
}
#[test]
fn test_inc_to_add() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::INC as u32, vec![reg(1)], 1);
assert!(c.try_inc_dec_to_add_sub(&mut inst));
assert_eq!(inst.opcode, X86Opcode::ADD as u32);
}
#[test]
fn test_dec_to_sub() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::DEC as u32, vec![reg(1)], 1);
assert!(c.try_inc_dec_to_add_sub(&mut inst));
assert_eq!(inst.opcode, X86Opcode::SUB as u32);
}
#[test]
fn test_cmov_same_src() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::CMOVE as u32, vec![reg(3), reg(3)], 3);
assert!(c.try_cmov_same_src(&mut inst));
assert_eq!(inst.opcode, X86Opcode::NOP as u32);
}
#[test]
fn test_cmov_different() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::CMOVE as u32, vec![reg(3), reg(4)], 3);
assert!(!c.try_cmov_same_src(&mut inst));
}
#[test]
fn test_and_same_reg() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::AND as u32, vec![reg(1), reg(1)], 1);
assert!(c.try_and_or_same_reg(&mut inst));
assert_eq!(inst.opcode, X86Opcode::NOP as u32);
}
#[test]
fn test_or_same_reg() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::OR as u32, vec![reg(2), reg(2)], 2);
assert!(c.try_and_or_same_reg(&mut inst));
assert_eq!(inst.opcode, X86Opcode::NOP as u32);
}
#[test]
fn test_lea_nop() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::LEA as u32, vec![reg(1), reg(1), imm(0)], 1);
assert!(c.try_lea_nop(&mut inst));
assert_eq!(inst.opcode, X86Opcode::NOP as u32);
}
#[test]
fn test_lea_not_nop_different_regs() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::LEA as u32, vec![reg(1), reg(2), imm(0)], 1);
assert!(!c.try_lea_nop(&mut inst));
}
#[test]
fn test_lea_to_mov() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::LEA as u32, vec![reg(1), reg(2), imm(0)], 1);
assert!(c.try_lea_to_mov(&mut inst));
assert_eq!(inst.opcode, X86Opcode::MOV as u32);
}
#[test]
fn test_lea_to_mov_same_reg_no_change() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::LEA as u32, vec![reg(1), reg(1), imm(0)], 1);
assert!(!c.try_lea_to_mov(&mut inst));
}
#[test]
fn test_shift_shift_combine() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::SHL as u32, vec![reg(1), imm(1)], 1);
let mut b = mi(X86Opcode::SHL as u32, vec![reg(1), imm(2)], 3);
assert!(c.try_shift_shift_combine(&mut a, &mut b));
assert_eq!(b.operands[1], MachineOperand::Imm(3));
assert_eq!(a.opcode, X86Opcode::NOP as u32);
}
#[test]
fn test_shift_overshift() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::SHL as u32, vec![reg(1), imm(60)], 1);
let mut b = mi(X86Opcode::SHL as u32, vec![reg(1), imm(10)], 3);
assert!(!c.try_shift_shift_combine(&mut a, &mut b));
assert_eq!(b.operands[1], MachineOperand::Imm(10));
}
#[test]
fn test_fma_formation_mulps_addps() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::VMULPS as u32, vec![reg(1), reg(2), reg(3)], 10);
let mut b = mi(X86Opcode::VADDPS as u32, vec![reg(4), reg(10), reg(5)], 11);
assert!(c.try_fma_formation(&mut a, &mut b));
assert_eq!(b.opcode, X86Opcode::VFMADD213PS as u32);
assert_eq!(a.opcode, X86Opcode::NOP as u32);
}
#[test]
fn test_fma_no_match() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::VMULPS as u32, vec![reg(1), reg(2), reg(3)], 10);
let mut b = mi(X86Opcode::VADDPS as u32, vec![reg(4), reg(99), reg(5)], 11);
assert!(!c.try_fma_formation(&mut a, &mut b));
}
#[test]
fn test_test_cmp_combine() {
let c = X86MachineCombine::new();
let a = mi_nodef(X86Opcode::TEST as u32, vec![reg(1), reg(1)]);
let b = mi_nodef(X86Opcode::CMP as u32, vec![reg(1), imm(0)]);
assert!(c.try_test_cmp_combine(&a, &b));
}
#[test]
fn test_cmp_zero_to_test() {
let c = X86MachineCombine::new();
let a = mi_nodef(X86Opcode::CMP as u32, vec![reg(1), imm(0)]);
let b = mi_nodef(X86Opcode::JE as u32, vec![label("L1")]);
assert!(c.try_cmp_zero_to_test(&a, &b));
}
#[test]
fn test_store_load_forward() {
let mut c = X86MachineCombine::new();
let mut store = mi(X86Opcode::MOV as u32, vec![label("addr"), reg(5)], 0);
let mut load = mi(X86Opcode::MOV as u32, vec![reg(3), label("addr")], 3);
assert!(c.try_store_load_forward(&mut store, &mut load));
assert_eq!(load.operands, vec![reg(5)]);
}
#[test]
fn test_store_load_forward_different_addr() {
let mut c = X86MachineCombine::new();
let mut store = mi(X86Opcode::MOV as u32, vec![label("addr1"), reg(5)], 0);
let mut load = mi(X86Opcode::MOV as u32, vec![reg(3), label("addr2")], 3);
assert!(!c.try_store_load_forward(&mut store, &mut load));
}
#[test]
fn test_cmp_setcc_zero() {
let mut c = X86MachineCombine::new();
let mut cmp = mi_nodef(X86Opcode::CMP as u32, vec![reg(1), imm(0)]);
let mut sete = mi(X86Opcode::SETE as u32, vec![reg(2)], 2);
assert!(c.try_cmp_setcc_combine(&mut cmp, &mut sete));
assert_eq!(cmp.opcode, X86Opcode::TEST as u32);
}
#[test]
fn test_redundant_mov_same_reg() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::MOV as u32, vec![reg(1)], 1);
assert!(c.try_redundant_mov(&mut inst));
assert_eq!(inst.opcode, X86Opcode::NOP as u32);
}
#[test]
fn test_redundant_mov_different() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::MOV as u32, vec![reg(2)], 1);
assert!(!c.try_redundant_mov(&mut inst));
}
#[test]
fn test_shl_add_to_lea() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::SHL as u32, vec![reg(2), imm(2)], 5);
let mut b = mi(X86Opcode::ADD as u32, vec![reg(1), reg(5)], 6);
assert!(c.try_shl_add_to_lea(&mut a, &mut b));
assert_eq!(b.opcode, X86Opcode::LEA as u32);
}
#[test]
fn test_imm_fold_into_cmp() {
let mut c = X86MachineCombine::new();
c.dead_regs.insert(10);
let mut a = mi(X86Opcode::MOV as u32, vec![imm(42)], 10);
let mut b = mi_nodef(X86Opcode::CMP as u32, vec![reg(10), reg(5)]);
assert!(c.try_imm_fold_into_cmp(&mut a, &mut b));
assert_eq!(b.operands[0], MachineOperand::Imm(42));
}
#[test]
fn test_load_store_memcpy() {
let mut c = X86MachineCombine::new();
let load = mi(X86Opcode::MOV as u32, vec![label("src"), reg(5)], 5);
let mid = mi_nodef(X86Opcode::NOP as u32, vec![]);
let store = mi(X86Opcode::MOV as u32, vec![reg(5), label("dst")], 0);
assert!(c.try_load_store_memcpy(&load, &mid, &store));
assert_eq!(c.stats.load_store_memcpy, 1);
}
#[test]
fn test_compose_shuffle_identity() {
let result = compose_shuffle_imm(0b11_10_01_00, 0b11_10_01_00);
assert_eq!(result, 0b11_10_01_00);
}
#[test]
fn test_compose_shuffle_swap() {
let first: u8 = 0b10_11_00_01;
let second: u8 = 0b11_10_01_00;
assert_eq!(compose_shuffle_imm(first, second), first);
}
#[test]
fn test_clear() {
let mut c = X86MachineCombine::new();
c.stats.add_add_to_lea = 5;
c.dead_regs.insert(1);
c.clear();
assert_eq!(c.stats.add_add_to_lea, 0);
assert!(c.dead_regs.is_empty());
}
#[test]
fn test_make_combiner() {
let c = make_x86_machine_combine();
assert!(c.enable_fma_formation);
assert_eq!(c.max_iterations, 8);
}
#[test]
fn test_stats_merge() {
let mut a = CombinerStats::default();
a.add_add_to_lea = 3;
a.fma_formation = 2;
let b = CombinerStats {
add_add_to_lea: 1,
fma_formation: 3,
..Default::default()
};
a.merge(&b);
assert_eq!(a.add_add_to_lea, 4);
assert_eq!(a.fma_formation, 5);
}
#[test]
fn test_stats_summary() {
let s = CombinerStats {
total_combined: 42,
fma_formation: 3,
..Default::default()
};
let summary = s.summary();
assert!(summary.contains("42"));
assert!(summary.contains("3"));
}
#[test]
fn test_add_add_to_lea() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::ADD as u32, vec![reg(1), reg(2)], 5);
let mut b = mi(X86Opcode::ADD as u32, vec![reg(3), reg(5)], 6);
assert!(c.try_add_add_to_lea(&mut a, &mut b));
assert_eq!(b.opcode, X86Opcode::LEA as u32);
}
#[test]
fn test_xor_add_to_lea() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::XOR as u32, vec![reg(5), reg(5)], 5);
let mut b = mi(X86Opcode::ADD as u32, vec![reg(1), reg(5)], 6);
assert!(c.try_xor_add_to_lea(&mut a, &mut b));
assert_eq!(b.opcode, X86Opcode::LEA as u32);
}
#[test]
fn test_build_dead_regs() {
let mut c = X86MachineCombine::new();
let block = MachineBasicBlock {
name: "test".into(),
instructions: vec![
mi(X86Opcode::MOV as u32, vec![imm(42)], 1),
mi(X86Opcode::ADD as u32, vec![reg(1), reg(2)], 3),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
};
c.build_dead_regs(&block);
assert!(c.dead_regs.contains(&3));
assert!(!c.dead_regs.contains(&1));
}
#[test]
fn test_vperm_combine() {
let mut c = X86MachineCombine::new();
let mut a = mi(
X86Opcode::VPERMILPS as u32,
vec![reg(0), reg(1), imm(0xE4)],
10,
);
let mut b = mi(
X86Opcode::VPERMILPS as u32,
vec![reg(3), reg(10), imm(0xE4)],
11,
);
assert!(c.try_vperm_combine(&mut a, &mut b));
assert_eq!(b.operands[1], MachineOperand::Reg(1));
}
#[test]
fn test_run_on_empty_function() {
let mut c = X86MachineCombine::new();
let mut mf = MachineFunction::new("empty");
let info = X86InstrInfo::new();
assert!(!c.run(&mut mf, &info));
}
#[test]
fn test_combine_block_with_multiple_rules() {
let mut c = X86MachineCombine::new();
let mut block = MachineBasicBlock {
name: "test".into(),
instructions: vec![
mi(X86Opcode::XOR as u32, vec![reg(1), reg(1)], 1),
mi(X86Opcode::INC as u32, vec![reg(2)], 2),
mi(X86Opcode::DEC as u32, vec![reg(3)], 3),
mi(X86Opcode::AND as u32, vec![reg(4), reg(4)], 4),
mi(X86Opcode::OR as u32, vec![reg(5), reg(5)], 5),
],
successors: vec![],
};
assert!(c.combine_block(&mut block));
assert!(c.stats.total_combined > 0);
}
#[test]
fn test_combine_block_no_changes() {
let mut c = X86MachineCombine::new();
let mut block = MachineBasicBlock {
name: "test".into(),
instructions: vec![
mi_nodef(X86Opcode::NOP as u32, vec![]),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
};
assert!(!c.combine_block(&mut block));
}
#[test]
fn test_run_on_multiple_blocks() {
let mut c = X86MachineCombine::new();
let mut mf = MachineFunction::new("test");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![
mi(X86Opcode::XOR as u32, vec![reg(0), reg(0)], 0),
mi(X86Opcode::INC as u32, vec![reg(0)], 0),
],
successors: vec!["exit".into()],
});
mf.blocks.push(MachineBasicBlock {
name: "exit".into(),
instructions: vec![mi_nodef(X86Opcode::RET as u32, vec![])],
successors: vec![],
});
let info = X86InstrInfo::new();
assert!(c.run(&mut mf, &info));
assert!(c.stats.total_combined > 0);
}
#[test]
fn test_add_add_to_lea_no_match() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::ADD as u32, vec![reg(1), reg(2)], 3);
let mut b = mi(X86Opcode::SUB as u32, vec![reg(3), reg(5)], 6);
assert!(!c.try_add_add_to_lea(&mut a, &mut b));
}
#[test]
fn test_shl_add_to_lea_invalid_shift() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::SHL as u32, vec![reg(2), imm(4)], 5);
let mut b = mi(X86Opcode::ADD as u32, vec![reg(1), reg(5)], 6);
assert!(!c.try_shl_add_to_lea(&mut a, &mut b));
}
#[test]
fn test_mov_op_to_mem_op_no_dead_reg() {
let mut c = X86MachineCombine::new();
c.dead_regs.insert(5);
let mut a = mi(X86Opcode::MOV as u32, vec![reg(1)], 5);
let mut b = mi(X86Opcode::ADD as u32, vec![reg(5), reg(2)], 3);
assert!(!c.try_mov_op_to_mem_op(&mut a, &mut b));
}
#[test]
fn test_cmov_all_variants() {
let cmovs = [
X86Opcode::CMOVO,
X86Opcode::CMOVNO,
X86Opcode::CMOVB,
X86Opcode::CMOVAE,
X86Opcode::CMOVE,
X86Opcode::CMOVNE,
X86Opcode::CMOVBE,
X86Opcode::CMOVA,
X86Opcode::CMOVS,
X86Opcode::CMOVNS,
X86Opcode::CMOVP,
X86Opcode::CMOVNP,
X86Opcode::CMOVL,
X86Opcode::CMOVGE,
X86Opcode::CMOVLE,
X86Opcode::CMOVG,
];
for op in &cmovs {
let mut c = X86MachineCombine::new();
let mut inst = mi(*op as u32, vec![reg(1), reg(1)], 1);
assert!(c.try_cmov_same_src(&mut inst), "CMOV {:?}", op);
assert_eq!(inst.opcode, X86Opcode::NOP as u32);
}
}
#[test]
fn test_broadcast_op_recognition() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::VBROADCASTSS as u32, vec![label("mem")], 10);
let mut b = mi(
X86Opcode::VADDPS as u32,
vec![reg(10), reg(11), reg(12)],
13,
);
c.try_broadcast_op(&mut a, &mut b);
assert_eq!(c.stats.broadcast_op, 1);
}
#[test]
fn test_broadcast_op_no_match() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::VBROADCASTSS as u32, vec![label("mem")], 10);
let mut b = mi(
X86Opcode::VANDPS as u32,
vec![reg(11), reg(12), reg(13)],
14,
);
c.try_broadcast_op(&mut a, &mut b);
assert_eq!(c.stats.broadcast_op, 0);
}
#[test]
fn test_cmp_setcc_combine_no_zero() {
let mut c = X86MachineCombine::new();
let mut cmp = mi_nodef(X86Opcode::CMP as u32, vec![reg(1), imm(5)]);
let mut sete = mi(X86Opcode::SETE as u32, vec![reg(2)], 2);
c.try_cmp_setcc_combine(&mut cmp, &mut sete);
assert_eq!(cmp.opcode, X86Opcode::CMP as u32);
}
#[test]
fn test_cmp_zero_to_test_not_cmp() {
let c = X86MachineCombine::new();
let a = mi_nodef(X86Opcode::ADD as u32, vec![reg(1), imm(0)]);
let b = mi_nodef(X86Opcode::JE as u32, vec![label("L")]);
assert!(!c.try_cmp_zero_to_test(&a, &b));
}
#[test]
fn test_fma_formation_scalar_single() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::MULSS as u32, vec![reg(1), reg(2)], 5);
let mut b = mi(X86Opcode::ADDSS as u32, vec![reg(5), reg(3)], 6);
assert!(c.try_fma_formation(&mut a, &mut b));
assert_eq!(b.opcode, X86Opcode::VFMADD213SS as u32);
}
#[test]
fn test_fma_formation_scalar_double() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::MULSD as u32, vec![reg(1), reg(2)], 5);
let mut b = mi(X86Opcode::ADDSD as u32, vec![reg(5), reg(3)], 6);
assert!(c.try_fma_formation(&mut a, &mut b));
assert_eq!(b.opcode, X86Opcode::VFMADD213SD as u32);
}
#[test]
fn test_fma_formation_sse_packed() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::MULPS as u32, vec![reg(0), reg(1)], 10);
let mut b = mi(X86Opcode::ADDPS as u32, vec![reg(10), reg(2)], 11);
assert!(c.try_fma_formation(&mut a, &mut b));
assert_eq!(b.opcode, X86Opcode::VFMADD213PS as u32);
}
#[test]
fn test_fma_disabled() {
let mut c = X86MachineCombine::new();
c.enable_fma_formation = false;
let mut a = mi(X86Opcode::VMULPS as u32, vec![reg(1), reg(2)], 10);
let mut b = mi(X86Opcode::VADDPS as u32, vec![reg(10), reg(5)], 11);
assert!(!c.try_fma_formation(&mut a, &mut b));
}
#[test]
fn test_vperm_disabled() {
let mut c = X86MachineCombine::new();
c.enable_vperm_combine = false;
let mut a = mi(
X86Opcode::VPERMILPS as u32,
vec![reg(0), reg(1), imm(0xE4)],
10,
);
let mut b = mi(
X86Opcode::VPERMILPS as u32,
vec![reg(3), reg(10), imm(0xE4)],
11,
);
assert!(!c.try_vperm_combine(&mut a, &mut b));
}
#[test]
fn test_store_load_forward_disabled() {
let mut c = X86MachineCombine::new();
c.enable_store_load_forward = false;
let mut store = mi(X86Opcode::MOV as u32, vec![label("addr"), reg(5)], 0);
let mut load = mi(X86Opcode::MOV as u32, vec![reg(3), label("addr")], 3);
assert!(!c.try_store_load_forward(&mut store, &mut load));
}
#[test]
fn test_imm_fold_cmp_disabled() {
let mut c = X86MachineCombine::new();
c.enable_cmp_optimization = false;
let mut a = mi(X86Opcode::MOV as u32, vec![imm(42)], 10);
let mut b = mi_nodef(X86Opcode::CMP as u32, vec![reg(10), reg(5)]);
assert!(!c.try_imm_fold_into_cmp(&mut a, &mut b));
}
#[test]
fn test_lea_to_mov_with_disp() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::LEA as u32, vec![reg(1), reg(2), imm(8)], 1);
assert!(!c.try_lea_to_mov(&mut inst));
}
#[test]
fn test_lea_to_mov_with_index() {
let mut c = X86MachineCombine::new();
let mut inst = mi(
X86Opcode::LEA as u32,
vec![reg(1), reg(2), imm(0), reg(3)],
1,
);
assert!(!c.try_lea_to_mov(&mut inst));
}
#[test]
fn test_compose_shuffle_double_swap() {
let swap: u8 = 0b10_11_00_01;
let result = compose_shuffle_imm(swap, swap);
assert_eq!(result, 0b11_10_01_00);
}
#[test]
fn test_make_aggressive_combiner() {
let c = make_x86_machine_combine_aggressive();
assert!(c.enable_fma_formation);
}
#[test]
fn test_combine_block_convergence() {
let mut c = X86MachineCombine::new();
c.max_iterations = 10;
let mut mf = MachineFunction::new("conv");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![
mi(X86Opcode::SHL as u32, vec![reg(0), imm(1)], 1),
mi(X86Opcode::SHL as u32, vec![reg(1), imm(2)], 2),
mi(X86Opcode::SHL as u32, vec![reg(2), imm(3)], 3),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
});
let info = X86InstrInfo::new();
c.run(&mut mf, &info);
}
#[test]
fn test_dead_regs_with_phys_regs() {
let mut c = X86MachineCombine::new();
let block = MachineBasicBlock {
name: "test".into(),
instructions: vec![
{
let mut inst = MachineInstr::new(X86Opcode::MOV as u32);
inst.def = Some(10);
inst.operands = vec![MachineOperand::PhysReg(1)];
inst
},
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
};
c.build_dead_regs(&block);
assert!(c.dead_regs.contains(&10));
}
#[test]
fn test_dead_regs_with_ret_removal() {
let mut c = X86MachineCombine::new();
let block = MachineBasicBlock {
name: "test".into(),
instructions: vec![
mi(X86Opcode::MOV as u32, vec![imm(5)], 7),
mi_nodef(X86Opcode::RET as u32, vec![reg(7)]),
],
successors: vec![],
};
c.build_dead_regs(&block);
assert!(!c.dead_regs.contains(&7));
}
}
#[test]
fn test_shift_combine_different_ops() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::SHL as u32, vec![reg(1), imm(1)], 1);
let mut b = mi(X86Opcode::SHR as u32, vec![reg(1), imm(2)], 3);
assert!(!c.try_shift_shift_combine(&mut a, &mut b));
}
#[test]
fn test_shift_combine_not_connected() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::SHL as u32, vec![reg(1), imm(1)], 1);
let mut b = mi(X86Opcode::SHL as u32, vec![reg(2), imm(3)], 4);
assert!(!c.try_shift_shift_combine(&mut a, &mut b));
}
#[test]
fn test_vperm_different_ops() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::VPERMILPS as u32, vec![reg(0), reg(1), imm(0xE4)], 10);
let mut b = mi(X86Opcode::VPERMPD as u32, vec![reg(3), reg(10), imm(0xE4)], 11);
assert!(!c.try_vperm_combine(&mut a, &mut b));
}
#[test]
fn test_vperm_not_connected() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::VPERMILPS as u32, vec![reg(0), reg(1), imm(0x44)], 10);
let mut b = mi(X86Opcode::VPERMILPS as u32, vec![reg(3), reg(99), imm(0xE4)], 11);
assert!(!c.try_vperm_combine(&mut a, &mut b));
}
#[test]
fn test_load_store_memcpy_no_match() {
let mut c = X86MachineCombine::new();
let a = mi(X86Opcode::ADD as u32, vec![reg(1), reg(2)], 3);
let mid = mi_nodef(X86Opcode::NOP as u32, vec![]);
let store = mi(X86Opcode::MOV as u32, vec![reg(3), label("dst")], 0);
assert!(!c.try_load_store_memcpy(&a, &mid, &store));
}
#[test]
fn test_load_store_memcpy_store_not_mov() {
let mut c = X86MachineCombine::new();
let load = mi(X86Opcode::MOV as u32, vec![label("src"), reg(5)], 5);
let mid = mi_nodef(X86Opcode::NOP as u32, vec![]);
let store = mi(X86Opcode::ADD as u32, vec![reg(5), reg(6)], 6);
assert!(!c.try_load_store_memcpy(&load, &mid, &store));
}
#[test]
fn test_horizontal_op_disabled() {
let mut c = X86MachineCombine::new();
c.enable_horizontal_ops = false;
let mut inst = mi(X86Opcode::HADDPS as u32, vec![reg(0), reg(1)], 2);
assert!(!c.try_horizontal_op(&mut inst));
}
#[test]
fn test_horizontal_op_hsub() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::HSUBPS as u32, vec![reg(0), reg(1)], 2);
assert!(c.try_horizontal_op(&mut inst));
assert_eq!(c.stats.sse_horizontal, 1);
}
#[test]
fn test_inc_dec_unchanged_on_non_inc_dec() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::ADD as u32, vec![reg(1), reg(2)], 1);
assert!(!c.try_inc_dec_to_add_sub(&mut inst));
}
#[test]
fn test_xor_zero_unchanged_on_non_xor() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::ADD as u32, vec![reg(1), reg(1)], 1);
assert!(!c.try_xor_zero_idiom(&mut inst));
}
#[test]
fn test_and_or_same_unchanged_on_different_regs() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::AND as u32, vec![reg(1), reg(2)], 3);
assert!(!c.try_and_or_same_reg(&mut inst));
}
#[test]
fn test_cmov_same_unchanged_on_non_cmov() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::MOV as u32, vec![reg(1), reg(1)], 1);
assert!(!c.try_cmov_same_src(&mut inst));
}
#[test]
fn test_lea_nop_unchanged_on_non_lea() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::MOV as u32, vec![reg(1)], 1);
assert!(!c.try_lea_nop(&mut inst));
}
#[test]
fn test_add_add_to_lea_reversed_not_matching() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::ADD as u32, vec![reg(1), reg(2)], 5);
let mut b = mi(X86Opcode::ADD as u32, vec![reg(5), reg(3)], 6);
assert!(!c.try_add_add_to_lea(&mut a, &mut b));
}
#[test]
fn test_imm_fold_not_mov() {
let mut c = X86MachineCombine::new();
c.dead_regs.insert(10);
let mut a = mi(X86Opcode::ADD as u32, vec![imm(42)], 10);
let mut b = mi_nodef(X86Opcode::CMP as u32, vec![reg(10), reg(5)]);
assert!(!c.try_imm_fold_into_cmp(&mut a, &mut b));
}
#[test]
fn test_stats_made_progress() {
let s = CombinerStats::default();
assert!(!s.made_progress());
let s2 = CombinerStats { total_combined: 1};
assert!(s2.made_progress());
}
#[test]
fn test_stats_summary_new_combiner() {
let c = X86MachineCombine::new();
let summary = c.stats.summary();
assert!(summary.contains("total=0"));
}
#[test]
fn test_combine_block_with_nops_unchanged() {
let mut c = X86MachineCombine::new();
let mut block = MachineBasicBlock {
name: "test".into(),
instructions: vec![
mi_nodef(X86Opcode::NOP as u32, vec![]),
mi_nodef(X86Opcode::NOP1 as u32, vec![]),
],
successors: vec![],
};
assert!(!c.combine_block(&mut block));
}
#[test]
fn test_redundant_mov_non_mov() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::LEA as u32, vec![reg(1), reg(1)], 1);
assert!(!c.try_redundant_mov(&mut inst));
}
#[test]
fn test_combine_block_empty() {
let mut c = X86MachineCombine::new();
let mut block = MachineBasicBlock {
name: "empty".into(),
instructions: vec![],
successors: vec![],
};
assert!(!c.combine_block(&mut block));
}
#[test]
fn test_combine_block_single_nop() {
let mut c = X86MachineCombine::new();
let mut block = MachineBasicBlock {
name: "test".into(),
instructions: vec![mi_nodef(X86Opcode::NOP as u32, vec![])],
successors: vec![],
};
assert!(!c.combine_block(&mut block));
}
#[test]
fn test_combine_block_iteration_converges() {
let mut c = X86MachineCombine::new();
c.max_iterations = 3;
let mut block = MachineBasicBlock {
name: "test".into(),
instructions: vec![
mi(X86Opcode::SHL as u32, vec![reg(0), imm(1)], 1),
mi(X86Opcode::SHL as u32, vec![reg(1), imm(1)], 2),
],
successors: vec![],
};
c.combine_block(&mut block);
}
#[test]
fn test_dead_regs_empty_block() {
let mut c = X86MachineCombine::new();
let block = MachineBasicBlock {
name: "empty".into(),
instructions: vec![],
successors: vec![],
};
c.build_dead_regs(&block);
assert!(c.dead_regs.is_empty());
}
#[test]
fn test_dead_regs_all_used() {
let mut c = X86MachineCombine::new();
let block = MachineBasicBlock {
name: "test".into(),
instructions: vec![
mi(X86Opcode::MOV as u32, vec![imm(5)], 1),
mi(X86Opcode::ADD as u32, vec![reg(1), reg(2)], 3),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
};
c.build_dead_regs(&block);
assert!(!c.dead_regs.contains(&1));
assert!(c.dead_regs.contains(&3));
}
#[test]
fn test_dead_regs_multiple_defs_same_reg() {
let mut c = X86MachineCombine::new();
let block = MachineBasicBlock {
name: "test".into(),
instructions: vec![
mi(X86Opcode::MOV as u32, vec![imm(5)], 1),
mi(X86Opcode::MOV as u32, vec![imm(10)], 1),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
};
c.build_dead_regs(&block);
assert!(c.dead_regs.contains(&1));
}
#[test]
fn test_lea_nop_with_scale() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::LEA as u32, vec![reg(1), reg(1), imm(0), imm(1)], 1);
assert!(!c.try_lea_nop(&mut inst));
}
#[test]
fn test_lea_to_mov_same_reg_no_change_2() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::LEA as u32, vec![reg(5), reg(5), imm(0)], 5);
assert!(!c.try_lea_to_mov(&mut inst));
assert!(c.try_lea_nop(&mut inst));
}
#[test]
fn test_cmp_zero_to_test_with_nonzero() {
let c = X86MachineCombine::new();
let a = mi_nodef(X86Opcode::CMP as u32, vec![reg(1), imm(5)]);
let b = mi_nodef(X86Opcode::JE as u32, vec![label("L")]);
assert!(!c.try_cmp_zero_to_test(&a, &b));
}
#[test]
fn test_test_cmp_combine_test_not_self() {
let c = X86MachineCombine::new();
let a = mi_nodef(X86Opcode::TEST as u32, vec![reg(1), imm(0xFF)]);
let b = mi_nodef(X86Opcode::CMP as u32, vec![reg(1), imm(0)]);
assert!(!c.try_test_cmp_combine(&a, &b));
}
#[test]
fn test_add_add_to_lea_with_same_b_src() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::ADD as u32, vec![reg(1), reg(2)], 5);
let mut b = mi(X86Opcode::ADD as u32, vec![reg(5), reg(3)], 6);
assert!(!c.try_add_add_to_lea(&mut a, &mut b));
}
#[test]
fn test_shl_add_to_lea_different_connection() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::SHL as u32, vec![reg(2), imm(1)], 5);
let mut b = mi(X86Opcode::ADD as u32, vec![reg(5), reg(3)], 6);
assert!(!c.try_shl_add_to_lea(&mut a, &mut b));
}
#[test]
fn test_xor_add_to_lea_not_xor_self() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::XOR as u32, vec![reg(5), reg(6)], 5);
let mut b = mi(X86Opcode::ADD as u32, vec![reg(1), reg(5)], 6);
assert!(!c.try_xor_add_to_lea(&mut a, &mut b));
}
#[test]
fn test_xor_add_to_lea_not_add() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::XOR as u32, vec![reg(5), reg(5)], 5);
let mut b = mi(X86Opcode::SUB as u32, vec![reg(1), reg(5)], 6);
assert!(!c.try_xor_add_to_lea(&mut a, &mut b));
}
#[test]
fn test_mov_op_to_mem_op_b_not_using_a() {
let mut c = X86MachineCombine::new();
c.dead_regs.insert(5);
let mut a = mi(X86Opcode::MOV as u32, vec![label("mem")], 5);
let mut b = mi(X86Opcode::ADD as u32, vec![reg(3), reg(2)], 7);
assert!(!c.try_mov_op_to_mem_op(&mut a, &mut b));
}
#[test]
fn test_shift_combine_different_shift_types() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::SHL as u32, vec![reg(1), imm(1)], 1);
let mut b = mi(X86Opcode::SHR as u32, vec![reg(1), imm(2)], 3);
assert!(!c.try_shift_shift_combine(&mut a, &mut b));
}
#[test]
fn test_shift_combine_negative_shift() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::SHL as u32, vec![reg(1), imm(-1)], 1);
let mut b = mi(X86Opcode::SHL as u32, vec![reg(1), imm(2)], 3);
assert!(!c.try_shift_shift_combine(&mut a, &mut b));
}
#[test]
fn test_combine_block_with_fma_chain() {
let mut c = X86MachineCombine::new();
let mut block = MachineBasicBlock {
name: "test".into(),
instructions: vec![
mi(X86Opcode::VMULPS as u32, vec![reg(0), reg(1)], 10),
mi(X86Opcode::VADDPS as u32, vec![reg(10), reg(2)], 11),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
};
let changed = c.combine_block(&mut block);
if changed {
assert!(c.stats.fma_formation > 0);
}
}
#[test]
fn test_combine_block_with_shift_chain() {
let mut c = X86MachineCombine::new();
let mut block = MachineBasicBlock {
name: "test".into(),
instructions: vec![
mi(X86Opcode::SHL as u32, vec![reg(0), imm(2)], 1),
mi(X86Opcode::SHL as u32, vec![reg(1), imm(3)], 2),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
};
c.combine_block(&mut block);
}
#[test]
fn test_combine_block_with_cmp_setcc() {
let mut c = X86MachineCombine::new();
let mut block = MachineBasicBlock {
name: "test".into(),
instructions: vec![
mi_nodef(X86Opcode::CMP as u32, vec![reg(0), imm(0)]),
mi(X86Opcode::SETE as u32, vec![reg(1)], 1),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
};
c.combine_block(&mut block);
}
#[test]
fn test_combine_block_with_inc_dec() {
let mut c = X86MachineCombine::new();
let mut block = MachineBasicBlock {
name: "test".into(),
instructions: vec![
mi(X86Opcode::INC as u32, vec![reg(0)], 1),
mi(X86Opcode::DEC as u32, vec![reg(1)], 2),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
};
let changed = c.combine_block(&mut block);
assert!(changed);
assert_eq!(c.stats.inc_dec_to_add_sub, 2);
}
#[test]
fn test_combine_block_with_nop_elimination() {
let mut c = X86MachineCombine::new();
let mut block = MachineBasicBlock {
name: "test".into(),
instructions: vec![
mi(X86Opcode::LEA as u32, vec![reg(0), reg(0), imm(0)], 0),
mi(X86Opcode::AND as u32, vec![reg(1), reg(1)], 1),
mi(X86Opcode::OR as u32, vec![reg(2), reg(2)], 2),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
};
let changed = c.combine_block(&mut block);
assert!(changed);
assert!(c.stats.lea_nop_elim > 0 || c.stats.and_or_same_to_nop > 0);
}
#[test]
fn test_run_function_with_single_block() {
let mut c = X86MachineCombine::new();
let mut mf = MachineFunction::new("test");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![
mi(X86Opcode::XOR as u32, vec![reg(0), reg(0)], 0),
mi(X86Opcode::INC as u32, vec![reg(1)], 1),
],
successors: vec![],
});
let info = X86InstrInfo::new();
assert!(c.run(&mut mf, &info));
}
#[test]
fn test_adjacent_load_store_different() {
let mut c = X86MachineCombine::new();
let a = mi(X86Opcode::MOVZX as u32, vec![label("a")], 1);
let b = mi(X86Opcode::MOVZX as u32, vec![label("b")], 2);
let c_inst = mi(X86Opcode::MOVZX as u32, vec![label("c")], 3);
c.try_adjacent_load_store(&a, &b, &c_inst);
assert!(c.stats.adjacent_load_store > 0);
}
#[test]
fn test_is_nop_like_variants() {
let nop_variants = [
X86Opcode::NOP, X86Opcode::NOP1, X86Opcode::NOP2, X86Opcode::NOP3,
X86Opcode::NOP4, X86Opcode::NOP5,
];
for op in &nop_variants {
let inst = mi_nodef(*op as u32, vec![]);
assert!(is_nop_like(&inst), "Expected {:?} to be NOP-like", op);
}
let not_nop = mi_nodef(X86Opcode::ADD as u32, vec![reg(1), reg(2)]);
assert!(!is_nop_like(¬_nop));
}
impl X86MachineCombine {
pub fn try_vzero_optimization(&mut self, a: &mut MachineInstr, b: &mut MachineInstr) -> bool {
if mi_opcode(a) == X86Opcode::VZEROALL as u32 && mi_opcode(b) == X86Opcode::VZEROUPPER as u32 {
b.opcode = X86Opcode::NOP as u32;
b.operands.clear();
b.def = None;
return true;
}
false
}
pub fn try_fold_load_into_test(&mut self, a: &mut MachineInstr, b: &mut MachineInstr) -> bool {
if !is_opcode(a, X86Opcode::MOV) || !is_opcode(b, X86Opcode::TEST) {
return false;
}
let a_def = match mi_def(a) { Some(r) => r, None => return false };
if !self.dead_regs.contains(&a_def) { return false; }
let mem = a.operands.iter().find(|op| matches!(op, MachineOperand::Label(_) | MachineOperand::Global(_))).cloned();
if let Some(mem_op) = mem {
b.operands[0] = mem_op;
a.opcode = X86Opcode::NOP as u32;
a.operands.clear();
a.def = None;
return true;
}
false
}
pub fn try_flag_reuse(&self, _a: &MachineInstr, _b: &MachineInstr) -> bool {
false
}
pub fn try_cmp_imm_fold(&mut self, a: &mut MachineInstr, b: &mut MachineInstr) -> bool {
if !self.enable_cmp_optimization { return false; }
if !is_opcode(a, X86Opcode::MOV) || !is_opcode(b, X86Opcode::CMP) { return false; }
let a_def = match mi_def(a) { Some(r) => r, None => return false };
if !self.dead_regs.contains(&a_def) { return false; }
let imm = match mi_imm_op(a, 0) { Some(v) => v, None => return false };
let uses_a = b.operands.iter().any(|op| matches!(op, MachineOperand::Reg(r) if *r == a_def));
if !uses_a { return false; }
if let MachineOperand::Imm(0) = b.operands[1] {
let flags_set = imm != 0;
if flags_set {
b.operands[0] = MachineOperand::Imm(imm);
b.operands[1] = b.operands[0].clone();
b.opcode = X86Opcode::TEST as u32;
} else {
b.opcode = X86Opcode::XOR as u32;
b.operands.clear();
b.operands.push(MachineOperand::Reg(a_def));
b.operands.push(MachineOperand::Reg(a_def));
}
a.opcode = X86Opcode::NOP as u32;
a.operands.clear();
a.def = None;
return true;
}
false
}
pub fn try_xor_test_combine(&self, a: &MachineInstr, b: &MachineInstr) -> bool {
if !is_opcode(a, X86Opcode::XOR) || !is_opcode(b, X86Opcode::TEST) { return false; }
if a.operands.len() >= 2 && a.operands[0] == a.operands[1] {
let a_def = match mi_def(a) { Some(r) => r, None => return false };
let b_uses = b.operands.iter().any(|op| matches!(op, MachineOperand::Reg(r) if *r == a_def));
if b_uses {
return true;
}
}
false
}
}
#[cfg(test)]
mod extra_tests {
use super::*;
fn imm(v: i64) -> MachineOperand { MachineOperand::Imm(v) }
fn reg(r: u32) -> MachineOperand { MachineOperand::Reg(r) }
fn label(s: &str) -> MachineOperand { MachineOperand::Label(s.to_string()) }
fn mi(opcode: u32, ops: Vec<MachineOperand>, def: u32) -> MachineInstr {
let mut i = MachineInstr::new(opcode);
i.operands = ops;
i.def = Some(def);
i
}
fn mi_nodef(opcode: u32, ops: Vec<MachineOperand>) -> MachineInstr {
let mut i = MachineInstr::new(opcode);
i.operands = ops;
i
}
#[test]
fn test_vzero_optimization() {
let mut c = X86MachineCombine::new();
let mut a = mi_nodef(X86Opcode::VZEROALL as u32, vec![]);
let mut b = mi_nodef(X86Opcode::VZEROUPPER as u32, vec![]);
assert!(c.try_vzero_optimization(&mut a, &mut b));
assert_eq!(b.opcode, X86Opcode::NOP as u32);
}
#[test]
fn test_vzero_no_optimization_when_not_pair() {
let mut c = X86MachineCombine::new();
let mut a = mi_nodef(X86Opcode::VZEROUPPER as u32, vec![]);
let mut b = mi_nodef(X86Opcode::VZEROALL as u32, vec![]);
assert!(!c.try_vzero_optimization(&mut a, &mut b));
}
#[test]
fn test_fold_load_into_test() {
let mut c = X86MachineCombine::new();
c.dead_regs.insert(5);
let mut a = mi(X86Opcode::MOV as u32, vec![label("mem")], 5);
let mut b = mi_nodef(X86Opcode::TEST as u32, vec![reg(5), reg(5)]);
assert!(c.try_fold_load_into_test(&mut a, &mut b));
assert_eq!(a.opcode, X86Opcode::NOP as u32);
}
#[test]
fn test_fold_load_into_test_reg_not_dead() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::MOV as u32, vec![label("mem")], 5);
let mut b = mi_nodef(X86Opcode::TEST as u32, vec![reg(5), reg(5)]);
assert!(!c.try_fold_load_into_test(&mut a, &mut b));
}
#[test]
fn test_fold_load_into_test_not_test() {
let mut c = X86MachineCombine::new();
c.dead_regs.insert(5);
let mut a = mi(X86Opcode::MOV as u32, vec![label("mem")], 5);
let mut b = mi_nodef(X86Opcode::CMP as u32, vec![reg(5), reg(6)]);
assert!(!c.try_fold_load_into_test(&mut a, &mut b));
}
#[test]
fn test_cmp_imm_fold_zero() {
let mut c = X86MachineCombine::new();
c.dead_regs.insert(10);
let mut a = mi(X86Opcode::MOV as u32, vec![imm(0)], 10);
let mut b = mi_nodef(X86Opcode::CMP as u32, vec![reg(10), imm(0)]);
assert!(c.try_cmp_imm_fold(&mut a, &mut b));
assert_eq!(b.opcode, X86Opcode::XOR as u32);
}
#[test]
fn test_cmp_imm_fold_nonzero() {
let mut c = X86MachineCombine::new();
c.dead_regs.insert(10);
let mut a = mi(X86Opcode::MOV as u32, vec![imm(42)], 10);
let mut b = mi_nodef(X86Opcode::CMP as u32, vec![reg(10), imm(0)]);
assert!(c.try_cmp_imm_fold(&mut a, &mut b));
assert_eq!(b.opcode, X86Opcode::TEST as u32);
}
#[test]
fn test_cmp_imm_fold_not_zero_literal() {
let mut c = X86MachineCombine::new();
c.dead_regs.insert(10);
let mut a = mi(X86Opcode::MOV as u32, vec![imm(42)], 10);
let mut b = mi_nodef(X86Opcode::CMP as u32, vec![reg(10), imm(5)]);
assert!(!c.try_cmp_imm_fold(&mut a, &mut b));
}
#[test]
fn test_xor_test_combine() {
let c = X86MachineCombine::new();
let a = mi(X86Opcode::XOR as u32, vec![reg(1), reg(1)], 1);
let b = mi_nodef(X86Opcode::TEST as u32, vec![reg(1), reg(1)]);
assert!(c.try_xor_test_combine(&a, &b));
}
#[test]
fn test_xor_test_combine_not_self_xor() {
let c = X86MachineCombine::new();
let a = mi(X86Opcode::XOR as u32, vec![reg(1), reg(2)], 1);
let b = mi_nodef(X86Opcode::TEST as u32, vec![reg(1), reg(1)]);
assert!(!c.try_xor_test_combine(&a, &b));
}
#[test]
fn test_xor_test_combine_not_test() {
let c = X86MachineCombine::new();
let a = mi(X86Opcode::XOR as u32, vec![reg(1), reg(1)], 1);
let b = mi_nodef(X86Opcode::CMP as u32, vec![reg(1), reg(1)]);
assert!(!c.try_xor_test_combine(&a, &b));
}
#[test]
fn test_run_with_vzero_optimization() {
let mut c = X86MachineCombine::new();
let mut mf = MachineFunction::new("test");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![
mi_nodef(X86Opcode::VZEROALL as u32, vec![]),
mi_nodef(X86Opcode::VZEROUPPER as u32, vec![]),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
});
let info = X86InstrInfo::new();
c.run(&mut mf, &info);
}
}
#[cfg(test)]
mod integration_tests {
use super::*;
fn mi(opcode: u32, ops: Vec<MachineOperand>, def: u32) -> MachineInstr {
let mut i = MachineInstr::new(opcode);
i.operands = ops;
i.def = Some(def);
i
}
fn mi_nodef(opcode: u32, ops: Vec<MachineOperand>) -> MachineInstr {
let mut i = MachineInstr::new(opcode);
i.operands = ops;
i
}
fn imm(v: i64) -> MachineOperand { MachineOperand::Imm(v) }
fn reg(r: u32) -> MachineOperand { MachineOperand::Reg(r) }
fn lbl(s: &str) -> MachineOperand { MachineOperand::Label(s.to_string()) }
#[test]
fn test_real_world_lea_pattern() {
let mut c = X86MachineCombine::new();
let mut mf = MachineFunction::new("lea_pattern");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![
mi(X86Opcode::SHL as u32, vec![reg(1), imm(2)], 5),
mi(X86Opcode::ADD as u32, vec![reg(0), reg(5)], 6),
mi(X86Opcode::MOV as u32, vec![reg(6)], 7),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
});
let info = X86InstrInfo::new();
c.run(&mut mf, &info);
}
#[test]
fn test_real_world_cmp_setcc() {
let mut c = X86MachineCombine::new();
let mut mf = MachineFunction::new("cmp_setcc");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![
mi_nodef(X86Opcode::CMP as u32, vec![reg(0), imm(0)]),
mi(X86Opcode::SETE as u32, vec![reg(1)], 1),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
});
let info = X86InstrInfo::new();
c.run(&mut mf, &info);
}
#[test]
fn test_real_world_fma_opportunity() {
let mut c = X86MachineCombine::new();
let mut mf = MachineFunction::new("fma_opp");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![
mi(X86Opcode::VMULPS as u32, vec![reg(1), reg(2)], 10),
mi(X86Opcode::VADDPS as u32, vec![reg(10), reg(3)], 11),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
});
let info = X86InstrInfo::new();
c.run(&mut mf, &info);
}
#[test]
fn test_multiple_fma_opportunities() {
let mut c = X86MachineCombine::new();
let mut mf = MachineFunction::new("multi_fma");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![
mi(X86Opcode::VMULPS as u32, vec![reg(1), reg(2)], 10),
mi(X86Opcode::VADDPS as u32, vec![reg(10), reg(3)], 11),
mi(X86Opcode::VMULPS as u32, vec![reg(4), reg(5)], 12),
mi(X86Opcode::VADDPS as u32, vec![reg(12), reg(6)], 13),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
});
let info = X86InstrInfo::new();
c.run(&mut mf, &info);
}
#[test]
fn test_nop_removal_chain() {
let mut c = X86MachineCombine::new();
let mut mf = MachineFunction::new("nop_chain");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![
mi(X86Opcode::LEA as u32, vec![reg(0), reg(0), imm(0)], 0),
mi(X86Opcode::AND as u32, vec![reg(0), reg(0)], 0),
mi(X86Opcode::OR as u32, vec![reg(0), reg(0)], 0),
mi(X86Opcode::CMOVE as u32, vec![reg(0), reg(0)], 0),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
});
let info = X86InstrInfo::new();
c.run(&mut mf, &info);
}
#[test]
fn test_store_load_forward_chain() {
let mut c = X86MachineCombine::new();
let mut mf = MachineFunction::new("store_load");
mf.blocks.push(MachineBasicBlock {
name: "entry".into(),
instructions: vec![
mi(X86Opcode::MOV as u32, vec![lbl("addr"), reg(5)], 0),
mi(X86Opcode::MOV as u32, vec![reg(3), lbl("addr")], 3),
mi_nodef(X86Opcode::RET as u32, vec![]),
],
successors: vec![],
});
let info = X86InstrInfo::new();
c.run(&mut mf, &info);
}
}
#[cfg(test)]
mod coverage_tests {
use super::*;
fn mi(opcode: u32, ops: Vec<MachineOperand>, def: u32) -> MachineInstr {
let mut i = MachineInstr::new(opcode);
i.operands = ops;
i.def = Some(def);
i
}
fn reg(r: u32) -> MachineOperand { MachineOperand::Reg(r) }
fn imm(v: i64) -> MachineOperand { MachineOperand::Imm(v) }
#[test]
fn test_shift_combine_with_non_immediate_count() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::SHL as u32, vec![reg(1), reg(2)], 1);
let mut b = mi(X86Opcode::SHL as u32, vec![reg(1), reg(3)], 3);
assert!(!c.try_shift_shift_combine(&mut a, &mut b));
}
#[test]
fn test_fma_formation_mul_then_add_commuted() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::VMULPS as u32, vec![reg(1), reg(2)], 5);
let mut b = mi(X86Opcode::VADDPS as u32, vec![reg(5), reg(3)], 6);
assert!(c.try_fma_formation(&mut a, &mut b));
}
#[test]
fn test_broadcast_with_mul() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::VBROADCASTSS as u32, vec![reg(1)], 10);
let mut b = mi(X86Opcode::VMULPS as u32, vec![reg(10), reg(2)], 11);
c.try_broadcast_op(&mut a, &mut b);
}
#[test]
fn test_broadcast_with_sub() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::VBROADCASTSD as u32, vec![reg(1)], 10);
let mut b = mi(X86Opcode::VSUBPD as u32, vec![reg(10), reg(2)], 11);
c.try_broadcast_op(&mut a, &mut b);
}
#[test]
fn test_vperm_with_different_opcodes() {
let mut c = X86MachineCombine::new();
let mut a = mi(X86Opcode::VPERMILPS as u32, vec![reg(0), reg(1), imm(0)], 10);
let mut b = mi(X86Opcode::VPERMQ as u32, vec![reg(3), reg(10), imm(0)], 11);
assert!(!c.try_vperm_combine(&mut a, &mut b));
}
#[test]
fn test_redundant_mov_not_dead() {
let mut c = X86MachineCombine::new();
let mut inst = mi(X86Opcode::MOV as u32, vec![reg(2)], 1);
assert!(!c.try_redundant_mov(&mut inst));
}
#[test]
fn test_dead_regs_complex_chain() {
let mut c = X86MachineCombine::new();
let block = MachineBasicBlock {
name: "chain".into(),
instructions: vec![
mi(X86Opcode::MOV as u32, vec![imm(1)], 1),
mi(X86Opcode::MOV as u32, vec![imm(2)], 2),
mi(X86Opcode::ADD as u32, vec![reg(1), reg(2)], 3),
mi(X86Opcode::MOV as u32, vec![imm(3)], 4),
],
successors: vec![],
};
c.build_dead_regs(&block);
assert!(!c.dead_regs.contains(&1));
assert!(!c.dead_regs.contains(&2));
assert!(c.dead_regs.contains(&3));
assert!(c.dead_regs.contains(&4));
}
#[test]
fn test_lea_nop_empty_operands() {
let mut c = X86MachineCombine::new();
let mut inst = MachineInstr::new(X86Opcode::LEA as u32);
inst.def = Some(0);
assert!(!c.try_lea_nop(&mut inst));
}
#[test]
fn test_cmov_no_def() {
let mut c = X86MachineCombine::new();
let mut inst = MachineInstr::new(X86Opcode::CMOVE as u32);
inst.operands = vec![reg(1), reg(1)];
assert!(!c.try_cmov_same_src(&mut inst));
}
#[test]
fn test_fma_no_mul_def() {
let mut c = X86MachineCombine::new();
let mut a = MachineInstr::new(X86Opcode::VMULPS as u32);
a.operands = vec![reg(1), reg(2)];
let mut b = mi(X86Opcode::VADDPS as u32, vec![reg(5), reg(3)], 5);
assert!(!c.try_fma_formation(&mut a, &mut b));
}
#[test]
fn test_vperm_no_def() {
let mut c = X86MachineCombine::new();
let mut a = MachineInstr::new(X86Opcode::VPERMILPS as u32);
a.operands = vec![reg(0), reg(1), imm(0)];
let mut b = mi(X86Opcode::VPERMILPS as u32, vec![reg(3), reg(10), imm(0)], 11);
assert!(!c.try_vperm_combine(&mut a, &mut b));
}
}