How can I improve compile times of very large code-generated Rust?
As part of my PhD I'm developing a compiler that takes formal models of instruction set architectures and produces fast simulators of those architectures. So far I can generate Rust code that, when compiled, executes the first instructions of the Linux kernel but the final program is very, very slow.
Unfortunately due to the volume of code generated (>2M LoC), I can only achieve reasonable compile times placing each function in a separate crate (2000 crates currently, 14000 expected) instead of a single file, enabling the parallel frontend, and using the dev profile.
If I enable opt-level=1 compile times grow from 2 minutes to >6 hours (exact time unknown as I haven't actually seen it terminate). From -Z time-passes I can see this is almost all LLVM.
Since Rust has no unstructured control flow, but my internal IR treats functions as a collection of basic blocks, I emit Rust functions which contain a variable storing the current basic block and use a loop and match statement to jump around. Below is an example of such a function, but the real issue are the functions with 1000s of blocks. I've observed that generally as the number of blocks grows, the compile time grows much faster.
My main question would be is there a better representation of this control flow in Rust that will compile faster with optimizations enabled? Does anyone have any other suggestions or advice for improving compile times/optimization? Thanks:)
pub fn SSAdvance<T: Tracer>(state: &mut State, tracer: &mut T, gs_24616: ()) -> () {
const BLOCK_0: u32 = 0u32;
const BLOCK_1: u32 = 1u32;
const BLOCK_2: u32 = 2u32;
const BLOCK_3: u32 = 3u32;
const BLOCK_4: u32 = 4u32;
const BLOCK_5: u32 = 5u32;
const BLOCK_6: u32 = 6u32;
const BLOCK_7: u32 = 7u32;
const BLOCK_8: u32 = 8u32;
let mut gs_24618: bool = Default::default();
let mut gs_24617: bool = Default::default();
let mut current_block = BLOCK_0;
loop {
match current_block {
BLOCK_0 => {
// C s_0_0: const #() : ()
let s_0_0: () = ();
// S s_0_1: call DebugTarget(s_0_0)
let s_0_1: u8 = DebugTarget(state, tracer, s_0_0);
// S s_0_2: call ELUsingAArch32(s_0_1)
let s_0_2: bool = ELUsingAArch32(state, tracer, s_0_1);
// S s_0_3: not s_0_2
let s_0_3: bool = !s_0_2;
// N s_0_4: branch s_0_3 b8 b1
current_block = if s_0_3 { BLOCK_8 } else { BLOCK_1 };
}
BLOCK_1 => {
// C s_1_0: const #0u : u1
let s_1_0: bool = false;
// D s_1_1: write-var gs#24617 <= s_1_0
gs_24617 = s_1_0;
// N s_1_2: jump b2
current_block = BLOCK_2;
}
BLOCK_2 => {
// D s_2_0: read-var gs#24617:u1
let s_2_0: bool = gs_24617;
// N s_2_1: branch s_2_0 b7 b3
current_block = if s_2_0 { BLOCK_7 } else { BLOCK_3 };
}
BLOCK_3 => {
// C s_3_0: const #0u : u1
let s_3_0: bool = false;
// D s_3_1: write-var gs#24618 <= s_3_0
gs_24618 = s_3_0;
// N s_3_2: jump b4
current_block = BLOCK_4;
}
BLOCK_4 => {
// D s_4_0: read-var gs#24618:u1
let s_4_0: bool = gs_24618;
// N s_4_1: branch s_4_0 b6 b5
current_block = if s_4_0 { BLOCK_6 } else { BLOCK_5 };
}
BLOCK_5 => {
// N s_5_0: return
return;
}
BLOCK_6 => {
// C s_6_0: const #0u : u1
let s_6_0: bool = false;
// C s_6_1: const #16856u : u64
let s_6_1: u64 = 16856;
// D s_6_2: read-reg s_6_1:struct
let s_6_2: CompositeTypec98939056e929b9c = {
let value = state
.read_register::<CompositeTypec98939056e929b9c>(s_6_1 as isize);
tracer.read_register(s_6_1 as usize, value);
value
};
// D s_6_3: mutate-field s_6_2.23 <= s_6_0
let s_6_3: CompositeTypec98939056e929b9c = {
let mut local = s_6_2.clone();
local._23 = s_6_0;
local
};
// N s_6_4: write-reg s_6_1 <= s_6_3
let s_6_4: () = {
state
.write_register::<
CompositeTypec98939056e929b9c,
>(s_6_1 as isize, s_6_3);
tracer.write_register(s_6_1 as usize, s_6_3);
};
// N s_6_5: return
return;
}
BLOCK_7 => {
// C s_7_0: const #16879u : u64
let s_7_0: u64 = 16879;
// D s_7_1: read-reg s_7_0:u1
let s_7_1: bool = {
let value = state.read_register::<bool>(s_7_0 as isize);
tracer.read_register(s_7_0 as usize, value);
value
};
// C s_7_2: const #1u : u8
let s_7_2: u8 = 1;
// D s_7_3: cast zx s_7_1 -> u64
let s_7_3: u64 = (s_7_1 as u64);
// D s_7_4: bundle s_7_3, s_7_2
let s_7_4: Bundle<u64, u8> = Bundle {
value: s_7_3,
length: s_7_2,
};
// C s_7_5: const #1u : u8
let s_7_5: u8 = 1;
// C s_7_6: const #1u : u64
let s_7_6: u64 = 1;
// D s_7_7: bundle s_7_6, s_7_5
let s_7_7: Bundle<u64, u8> = Bundle {
value: s_7_6,
length: s_7_5,
};
// D s_7_8: cmp-eq s_7_4 s_7_7
let s_7_8: bool = ((s_7_4) == (s_7_7));
// D s_7_9: write-var gs#24618 <= s_7_8
gs_24618 = s_7_8;
// N s_7_10: jump b4
current_block = BLOCK_4;
}
BLOCK_8 => {
// C s_8_0: const #103872u : u32
let s_8_0: u32 = 103872;
// D s_8_1: read-reg s_8_0:struct
let s_8_1: CompositeType5c790c8ef59cc8b2 = {
let value = state
.read_register::<CompositeType5c790c8ef59cc8b2>(s_8_0 as isize);
tracer.read_register(s_8_0 as usize, value);
value
};
// D s_8_2: call _get_MDSCR_EL1_Type_SS(s_8_1)
let s_8_2: bool = u_get_MDSCR_EL1_Type_SS(state, tracer, s_8_1);
// C s_8_3: const #1u : u8
let s_8_3: u8 = 1;
// D s_8_4: cast zx s_8_2 -> u64
let s_8_4: u64 = (s_8_2 as u64);
// D s_8_5: bundle s_8_4, s_8_3
let s_8_5: Bundle<u64, u8> = Bundle {
value: s_8_4,
length: s_8_3,
};
// C s_8_6: const #1u : u8
let s_8_6: u8 = 1;
// C s_8_7: const #1u : u64
let s_8_7: u64 = 1;
// D s_8_8: bundle s_8_7, s_8_6
let s_8_8: Bundle<u64, u8> = Bundle {
value: s_8_7,
length: s_8_6,
};
// D s_8_9: cmp-eq s_8_5 s_8_8
let s_8_9: bool = ((s_8_5) == (s_8_8));
// D s_8_10: write-var gs#24617 <= s_8_9
gs_24617 = s_8_9;
// N s_8_11: jump b2
current_block = BLOCK_2;
}
u => panic!("undefined block {u}"),
}
}
}