Skip to content

Commit 5923d3e

Browse files
perf: shrink binary by 10% (660K -> 594K) via dead-code removal and HashMap dedup
1 parent 258da4a commit 5923d3e

14 files changed

Lines changed: 144 additions & 166 deletions

File tree

compiler/src/main.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,27 @@ options:
2020
#[inline]
2121
fn eprint_msg(msg: &str) {
2222
use std::io::Write;
23-
let _ = writeln!(std::io::stderr(), "{}", msg);
23+
let mut e = std::io::stderr().lock();
24+
let _ = e.write_all(msg.as_bytes());
25+
let _ = e.write_all(b"\n");
2426
}
2527

2628
#[inline]
2729
fn print_msg(level: &str, msg: &str) {
2830
use std::io::Write;
29-
let _ = writeln!(std::io::stdout(), "[{}] {}", level, msg);
31+
let mut o = std::io::stdout().lock();
32+
let _ = o.write_all(b"[");
33+
let _ = o.write_all(level.as_bytes());
34+
let _ = o.write_all(b"] ");
35+
let _ = o.write_all(msg.as_bytes());
36+
let _ = o.write_all(b"\n");
3037
}
3138

3239
fn parse_args() -> (String, usize, bool, bool) {
3340
let args: Vec<_> = env::args().skip(1).collect();
3441
if args.is_empty() || args.iter().any(|a| a == "-h") {
35-
print!("{}", HELP);
42+
use std::io::Write;
43+
let _ = std::io::stdout().lock().write_all(HELP.as_bytes());
3644
exit(0);
3745
}
3846
let q = args.iter().any(|a| a == "-q");
@@ -76,7 +84,14 @@ fn run(path: &str, sandbox: bool, verbosity: usize, quiet: bool) -> Result<(), S
7684
let mut vm = VM::with_limits(&chunk, limits);
7785
let exec_result = vm.run();
7886

79-
vm.output.iter().for_each(|l| println!("{l}"));
87+
{
88+
use std::io::Write;
89+
let mut out = std::io::stdout().lock();
90+
for l in &vm.output {
91+
let _ = out.write_all(l.as_bytes());
92+
let _ = out.write_all(b"\n");
93+
}
94+
}
8095

8196
if let Err(e) = exec_result {
8297
return Err(e.render());

compiler/src/modules/parser/expr.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ impl<'src, I: Iterator<Item = Token>> Parser<'src, I> {
386386
s.chunk.emit(OpCode::ReturnValue, 0);
387387
});
388388

389-
let param_slots: alloc::collections::BTreeSet<String> = params.iter().map(|p| s!(str p.trim_start_matches('*'), "_0")).collect();
389+
let param_slots: crate::modules::fx::FxHashSet<String> = params.iter().map(|p| s!(str p.trim_start_matches('*'), "_0")).collect();
390390
for name in &body.names {
391391
if !param_slots.contains(name.as_str()) {
392392
self.chunk.push_name(name);

compiler/src/modules/parser/literals.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ impl<'src, I: Iterator<Item = Token>> Parser<'src, I> {
134134
for_iters.push(fi);
135135
}
136136

137-
let mut var_map: HashMap<u16, u16> = HashMap::default();
137+
// Tiny mapping (typical size 1-5); linear scan beats HashMap and
138+
// avoids monomorphizing a hash table for u16 keys.
139+
let mut var_map: Vec<(u16, u16)> = Vec::new();
138140
for var in &all_vars {
139141
let old_ver = versions_before.get(var).copied().unwrap_or(0);
140142
let new_ver = self.current_version(var);
@@ -144,13 +146,13 @@ impl<'src, I: Iterator<Item = Token>> Parser<'src, I> {
144146
let Some(&old_slot) = self.chunk.name_index.get(old_name) else { continue };
145147
let mut nb = [0u8; 128];
146148
let new_slot = self.chunk.push_name(Self::ssa_name(var, new_ver, &mut nb));
147-
var_map.insert(old_slot, new_slot);
149+
var_map.push((old_slot, new_slot));
148150
}
149151

150152
for body in elem_bodies {
151153
for ins in body {
152154
let operand = if matches!(ins.opcode, OpCode::LoadName | OpCode::StoreName) {
153-
var_map.get(&ins.operand).copied().unwrap_or(ins.operand)
155+
var_map.iter().find(|(k, _)| *k == ins.operand).map(|(_, v)| *v).unwrap_or(ins.operand)
154156
} else {
155157
ins.operand
156158
};

compiler/src/modules/parser/types.rs

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -279,39 +279,3 @@ pub const BUILTIN_TYPES: &[&str] = &[
279279
"AssertionError", "ArithmeticError", "LookupError",
280280
];
281281

282-
// Functional grouping of opcodes for analysis tooling. Not used by the
283-
// runtime dispatch (which switches on OpCode directly).
284-
#[derive(Debug, Clone, Copy, PartialEq)]
285-
pub enum OpCategory {
286-
Load, Store,
287-
Arith, Bitwise, Compare, Logic, Identity,
288-
ControlFlow, Iter,
289-
Build, Container, Comprehension,
290-
Function, Ssa, Yield, Side,
291-
Unsupported,
292-
}
293-
294-
impl OpCode {
295-
pub fn category(self) -> OpCategory {
296-
use OpCode::*;
297-
match self {
298-
LoadConst | LoadName | LoadTrue | LoadFalse | LoadNone | LoadEllipsis => OpCategory::Load, StoreName => OpCategory::Store,
299-
Add | Sub | Mul | Div | Mod | Pow | FloorDiv | Minus => OpCategory::Arith, BitAnd | BitOr | BitXor | BitNot | Shl | Shr => OpCategory::Bitwise,
300-
Eq | NotEq | Lt | Gt | LtEq | GtEq => OpCategory::Compare, And | Or | Not => OpCategory::Logic,
301-
In | NotIn | Is | IsNot => OpCategory::Identity,
302-
Jump | JumpIfFalse | JumpIfFalseOrPop | JumpIfTrueOrPop | ReturnValue | PopTop | Dup | Dup2 => OpCategory::ControlFlow,
303-
GetIter | ForIter => OpCategory::Iter,
304-
BuildList | BuildTuple | BuildDict | BuildSet | BuildSlice | BuildString => OpCategory::Build,
305-
GetItem | StoreItem | UnpackSequence | UnpackEx | FormatValue => OpCategory::Container,
306-
ListAppend | SetAdd | MapAdd => OpCategory::Comprehension,
307-
Call | MakeFunction | MakeCoroutine | CallPrint | CallLen | CallAbs | CallStr | CallInt | CallRange | CallChr | CallType | CallFloat | CallBool | CallRound | CallMin | CallMax | CallSum | CallSorted | CallEnumerate | CallZip | CallList | CallTuple | CallDict | CallIsInstance | CallSet | CallInput | CallOrd | CallMethod | CallMethodArgs | CallAll | CallAny | CallBin | CallOct | CallHex | CallDivmod | CallPow | CallRepr | CallReversed | CallCallable | CallId | CallHash => OpCategory::Function,
308-
Phi => OpCategory::Ssa,
309-
Yield => OpCategory::Yield,
310-
Assert | Del | Global | Nonlocal | TypeAlias | Import | ImportFrom | SetupExcept | PopExcept | Raise | RaiseFrom | Await | YieldFrom => OpCategory::Side,
311-
LoadAttr => OpCategory::Load,
312-
SetupWith | ExitWith => OpCategory::ControlFlow,
313-
UnpackArgs => OpCategory::Container,
314-
MakeClass | StoreAttr => OpCategory::Unsupported,
315-
}
316-
}
317-
}

compiler/src/modules/vm/builtins.rs

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,30 @@ impl<'a> VM<'a> {
134134
self.push(v); Ok(())
135135
}
136136

137+
/* Allocate a List from items and push. Centralises the
138+
Rc::new(RefCell::new(items)) construction inlined ~15 times. */
139+
pub(crate) fn alloc_list(&mut self, items: Vec<Val>) -> Result<Val, VmErr> {
140+
self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(items))))
141+
}
142+
143+
/* Allocate a List, push it, return Ok. */
144+
pub(crate) fn alloc_and_push_list(&mut self, items: Vec<Val>) -> Result<(), VmErr> {
145+
let v = self.alloc_list(items)?;
146+
self.push(v); Ok(())
147+
}
148+
149+
/* Allocate a Dict from a DictMap and push. */
150+
pub(crate) fn alloc_and_push_dict(&mut self, dm: DictMap) -> Result<(), VmErr> {
151+
let v = self.heap.alloc(HeapObj::Dict(Rc::new(RefCell::new(dm))))?;
152+
self.push(v); Ok(())
153+
}
154+
155+
/* Allocate a Tuple and push. */
156+
pub(crate) fn alloc_and_push_tuple(&mut self, items: Vec<Val>) -> Result<(), VmErr> {
157+
let v = self.heap.alloc(HeapObj::Tuple(items))?;
158+
self.push(v); Ok(())
159+
}
160+
137161
pub fn call_int(&mut self) -> Result<(), VmErr> {
138162
let o = self.pop()?;
139163
if o.is_heap()
@@ -262,8 +286,7 @@ impl<'a> VM<'a> {
262286
let o = self.pop()?;
263287
let mut items = self.extract_iter(o, false)?;
264288
self.sort_by_lt(&mut items)?;
265-
let val = self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(items))))?;
266-
self.push(val); Ok(())
289+
self.alloc_and_push_list(items)
267290
}
268291

269292
/* In-place sort using lt_vals for ordering. Captures the first error
@@ -294,13 +317,10 @@ impl<'a> VM<'a> {
294317
&& let HeapObj::Str(s) = self.heap.get(o) {
295318
let s = s.clone();
296319
let items = self.str_to_char_vals(&s)?;
297-
let val = self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(items))))?;
298-
self.push(val);
299-
return Ok(());
320+
return self.alloc_and_push_list(items);
300321
}
301322
let items = self.extract_iter(o, true)?;
302-
let val = self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(items))))?;
303-
self.push(val); Ok(())
323+
self.alloc_and_push_list(items)
304324
}
305325

306326
pub fn call_tuple(&mut self) -> Result<(), VmErr> {
@@ -310,8 +330,7 @@ impl<'a> VM<'a> {
310330
HeapObj::List(v) => v.borrow().clone(),
311331
_ => return Err(cold_type("tuple() argument must be iterable")),
312332
}} else { return Err(cold_type("tuple() argument must be iterable")); };
313-
let val = self.heap.alloc(HeapObj::Tuple(items))?;
314-
self.push(val); Ok(())
333+
self.alloc_and_push_tuple(items)
315334
}
316335

317336
pub fn call_enumerate(&mut self) -> Result<(), VmErr> {
@@ -322,8 +341,7 @@ impl<'a> VM<'a> {
322341
let t = self.heap.alloc(HeapObj::Tuple(vec![Val::int(i as i64), x]))?;
323342
pairs.push(t);
324343
}
325-
let val = self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(pairs))))?;
326-
self.push(val); Ok(())
344+
self.alloc_and_push_list(pairs)
327345
}
328346

329347
/* Pairs elements from N iterables into tuples, truncating to the shortest. */
@@ -340,8 +358,7 @@ impl<'a> VM<'a> {
340358
let t = self.heap.alloc(HeapObj::Tuple(tuple))?;
341359
pairs.push(t);
342360
}
343-
let val = self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(pairs))))?;
344-
self.push(val); Ok(())
361+
self.alloc_and_push_list(pairs)
345362
}
346363

347364
/* Type-name based isinstance check. Accepts Type or NativeFn (for the
@@ -486,26 +503,22 @@ impl<'a> VM<'a> {
486503
}
487504
let mid = items.len() - after;
488505
for &v in items[mid..].iter().rev() { self.push(v); }
489-
let star = self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(
490-
items[before..mid].to_vec()
491-
))))?;
506+
let star = self.alloc_list(items[before..mid].to_vec())?;
492507
self.push(star);
493508
for &v in items[..before].iter().rev() { self.push(v); }
494509
Ok(())
495510
}
496511

497512
pub fn call_dict(&mut self, op: u16) -> Result<(), VmErr> {
498-
if op == 0 {
499-
let val = self.heap.alloc(HeapObj::Dict(Rc::new(RefCell::new(DictMap::new()))))?;
500-
self.push(val);
513+
let dm = if op == 0 {
514+
DictMap::new()
501515
} else {
502516
let args = self.pop_n((op as usize) * 2)?;
503517
let mut dm = DictMap::with_capacity(op as usize);
504518
for pair in args.chunks(2) { dm.insert(pair[0], pair[1]); }
505-
let val = self.heap.alloc(HeapObj::Dict(Rc::new(RefCell::new(dm))))?;
506-
self.push(val);
507-
}
508-
Ok(())
519+
dm
520+
};
521+
self.alloc_and_push_dict(dm)
509522
}
510523

511524
pub fn call_set(&mut self, op: u16) -> Result<(), VmErr> {
@@ -716,8 +729,7 @@ impl<'a> VM<'a> {
716729
let (q, r) = ba.divmod(&bb).ok_or(VmErr::ZeroDiv)?;
717730
let qv = self.bigint_to_val(q)?;
718731
let rv = self.bigint_to_val(r)?;
719-
let val = self.heap.alloc(HeapObj::Tuple(vec![qv, rv]))?;
720-
self.push(val); Ok(())
732+
self.alloc_and_push_tuple(vec![qv, rv])
721733
}
722734

723735
pub fn call_pow(&mut self, op: u16) -> Result<(), VmErr> {
@@ -846,8 +858,7 @@ impl<'a> VM<'a> {
846858
self.extract_iter(o, true)?
847859
};
848860
items.reverse();
849-
let v = self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(items))))?;
850-
self.push(v); Ok(())
861+
self.alloc_and_push_list(items)
851862
}
852863

853864
// format(value [, spec]).

compiler/src/modules/vm/cache.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use super::types::{Val, HeapObj, HeapPool, VmErr, BigInt, eq_vals_with_heap};
22
use crate::modules::parser::{OpCode, SSAChunk, Instruction, Value};
3-
use crate::modules::fx::FxHashMap as HashMap;
43

54
use alloc::{vec, vec::Vec, string::ToString};
65

@@ -149,21 +148,24 @@ fn hash_args(args: &[Val]) -> u64 {
149148
h
150149
}
151150

152-
pub struct Templates { map: HashMap<usize, Vec<TplEntry>> }
151+
// Indexed by `fi` (function id, dense from 0..N). Vec gives O(1) lookup
152+
// without a HashMap monomorphization.
153+
pub struct Templates { slots: Vec<Vec<TplEntry>> }
153154

154155
impl Templates {
155-
pub fn new() -> Self { Self { map: HashMap::default() } }
156+
pub fn new() -> Self { Self { slots: Vec::new() } }
156157

157158
pub fn lookup(&self, fi: usize, args: &[Val], heap: &super::types::HeapPool) -> Option<Val> {
158159
let h = hash_args(args);
159-
self.map.get(&fi)?.iter()
160+
self.slots.get(fi)?.iter()
160161
.find(|e| e.hits >= TPL_THRESH && args_match(e, args, h, heap))
161162
.map(|e| e.result)
162163
}
163164

164165
pub fn record(&mut self, fi: usize, args: &[Val], result: Val, heap: &super::types::HeapPool) {
166+
if self.slots.len() <= fi { self.slots.resize_with(fi + 1, Vec::new); }
165167
let h = hash_args(args);
166-
let v = self.map.entry(fi).or_default();
168+
let v = &mut self.slots[fi];
167169
if let Some(e) = v.iter_mut().find(|e| args_match(e, args, h, heap)) {
168170
e.hits += 1; e.result = result;
169171
} else if v.len() < 256 {
@@ -172,7 +174,7 @@ impl Templates {
172174
}
173175

174176
pub fn count(&self) -> usize {
175-
self.map.values().flat_map(|v| v.iter()).filter(|e| e.hits >= TPL_THRESH).count()
177+
self.slots.iter().flat_map(|v| v.iter()).filter(|e| e.hits >= TPL_THRESH).count()
176178
}
177179
}
178180

compiler/src/modules/vm/handlers/arith.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ impl<'a> VM<'a> {
2525
OpCode::Mod => self.exec_mod(a, b)?,
2626
OpCode::Pow => self.exec_pow(a, b)?,
2727
OpCode::FloorDiv => self.exec_floordiv(a, b)?,
28-
_ => unreachable!("non-arith opcode in handle_arith"),
28+
_ => return Err(cold_runtime("non-arith opcode in handle_arith")),
2929
};
3030
self.push(result);
3131
Ok(())
@@ -101,7 +101,7 @@ impl<'a> VM<'a> {
101101
OpCode::BitXor => self.bitwise_op(a, b, |x, y| x ^ y)?,
102102
OpCode::Shl => self.exec_shl(a, b)?,
103103
OpCode::Shr => self.exec_shr(a, b)?,
104-
_ => unreachable!("non-bitwise opcode in handle_bitwise"),
104+
_ => return Err(cold_runtime("non-bitwise opcode in handle_bitwise")),
105105
};
106106
self.push(result);
107107
Ok(())
@@ -140,7 +140,7 @@ impl<'a> VM<'a> {
140140
OpCode::Gt => self.lt_vals(b, a)?,
141141
OpCode::LtEq => !self.lt_vals(b, a)?,
142142
OpCode::GtEq => !self.lt_vals(a, b)?,
143-
_ => unreachable!("non-compare opcode in handle_compare"),
143+
_ => return Err(cold_runtime("non-compare opcode in handle_compare")),
144144
};
145145
self.push(Val::bool(result));
146146
Ok(())
@@ -154,7 +154,7 @@ impl<'a> VM<'a> {
154154
let v = self.pop()?;
155155
self.push(Val::bool(!self.truthy(v)));
156156
}
157-
_ => unreachable!("non-logic opcode in handle_logic"),
157+
_ => return Err(cold_runtime("non-logic opcode in handle_logic")),
158158
}
159159
Ok(())
160160
}
@@ -167,7 +167,7 @@ impl<'a> VM<'a> {
167167
OpCode::NotIn => !self.contains(b, a),
168168
OpCode::Is => a.0 == b.0,
169169
OpCode::IsNot => a.0 != b.0,
170-
_ => unreachable!("non-identity opcode in handle_identity"),
170+
_ => return Err(cold_runtime("non-identity opcode in handle_identity")),
171171
};
172172
self.push(Val::bool(result));
173173
Ok(())

compiler/src/modules/vm/handlers/data.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ impl<'a> VM<'a> {
3636
}
3737
OpCode::BuildSet => self.build_set(operand)?,
3838
OpCode::BuildSlice => self.build_slice(operand)?,
39-
_ => unreachable!("non-build opcode in handle_build"),
39+
_ => return Err(cold_runtime("non-build opcode in handle_build")),
4040
}
4141
Ok(())
4242
}
@@ -58,7 +58,7 @@ impl<'a> VM<'a> {
5858
let val = self.heap.alloc(HeapObj::Str(s))?;
5959
self.push(val);
6060
}
61-
_ => unreachable!("non-container opcode in handle_container"),
61+
_ => return Err(cold_runtime("non-container opcode in handle_container")),
6262
}
6363
Ok(())
6464
}
@@ -69,7 +69,7 @@ impl<'a> VM<'a> {
6969
OpCode::ListAppend => ("list", self.pop()?, None),
7070
OpCode::SetAdd => ("set", self.pop()?, None),
7171
OpCode::MapAdd => { let v = self.pop()?; let k = self.pop()?; ("dict", v, Some(k)) }
72-
_ => unreachable!("non-comprehension opcode in handle_comprehension"),
72+
_ => return Err(cold_runtime("non-comprehension opcode in handle_comprehension")),
7373
};
7474
let acc = *self.stack.last().ok_or(VmErr::Runtime("stack underflow"))?;
7575
let corrupt = || VmErr::Runtime(match kind {
@@ -137,7 +137,7 @@ impl<'a> VM<'a> {
137137
}
138138
}
139139
OpCode::YieldFrom => {}
140-
_ => unreachable!("non-side opcode in handle_side"),
140+
_ => return Err(cold_runtime("non-side opcode in handle_side")),
141141
}
142142
Ok(())
143143
}

0 commit comments

Comments
 (0)