Skip to content

Commit b8000fc

Browse files
authored
fix: Cache line alignment (#316)
1 parent 983b802 commit b8000fc

49 files changed

Lines changed: 737 additions & 498 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/plotnik-lib/src/bytecode/dump.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use super::ids::TypeId;
1212
use super::instructions::StepId;
1313
use super::ir::NodeTypeIR;
1414
use super::module::{Instruction, Module};
15+
use super::nav::Nav;
1516
use super::type_meta::{TypeData, TypeKind};
1617
use super::{Call, Match, Return, Trampoline};
1718

@@ -364,6 +365,30 @@ fn dump_entrypoints(out: &mut String, module: &Module, ctx: &DumpContext) {
364365
out.push('\n');
365366
}
366367

368+
/// Check if an instruction is padding (all-zeros Match8).
369+
///
370+
/// Padding slots contain zero bytes which decode as terminal epsilon Match8
371+
/// with Any node type, no field constraint, and next=0.
372+
fn is_padding(instr: &Instruction) -> bool {
373+
match instr {
374+
Instruction::Match(m) => {
375+
m.is_match8()
376+
&& m.nav == Nav::Epsilon
377+
&& matches!(m.node_type, NodeTypeIR::Any)
378+
&& m.node_field.is_none()
379+
&& m.is_terminal()
380+
}
381+
_ => false,
382+
}
383+
}
384+
385+
/// Format a single padding step line.
386+
///
387+
/// Output: ` 07 ... ` (step number and " ... " in symbol column)
388+
fn format_padding_step(step: u16, step_width: usize) -> String {
389+
LineBuilder::new(step_width).instruction_prefix(step, Symbol::PADDING)
390+
}
391+
367392
fn dump_code(out: &mut String, module: &Module, ctx: &DumpContext) {
368393
let c = &ctx.colors;
369394
let header = module.header();
@@ -386,6 +411,14 @@ fn dump_code(out: &mut String, module: &Module, ctx: &DumpContext) {
386411
}
387412

388413
let instr = module.decode_step(step);
414+
415+
// Check for padding (all-zeros Match8 instruction)
416+
if is_padding(&instr) {
417+
writeln!(out, "{}", format_padding_step(step, step_width)).unwrap();
418+
step += 1;
419+
continue;
420+
}
421+
389422
let line = format_instruction(step, &instr, module, ctx, step_width);
390423
out.push_str(&line);
391424
out.push('\n');

crates/plotnik-lib/src/bytecode/format.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ impl Symbol {
6262
/// Epsilon symbol for unconditional transitions.
6363
pub const EPSILON: Symbol = Symbol::new(" ", "ε", " ");
6464

65+
/// Padding indicator (centered "..." in 5-char column).
66+
pub const PADDING: Symbol = Symbol::new(" ", "...", " ");
67+
6568
/// Format as a 5-character string.
6669
pub fn format(&self) -> String {
6770
format!("{}{}{}", self.left, self.center, self.right)

crates/plotnik-lib/src/bytecode/instructions.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ impl<'a> Match<'a> {
216216
self.nav == Nav::Epsilon
217217
}
218218

219+
/// Check if this is a Match8 (8-byte fast-path instruction).
220+
#[inline]
221+
pub fn is_match8(&self) -> bool {
222+
self.is_match8
223+
}
224+
219225
/// Number of successors.
220226
#[inline]
221227
pub fn succ_count(&self) -> usize {

crates/plotnik-lib/src/emit/layout.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Cache-aligned instruction layout.
22
//!
3-
//! Uses Pettis-Hansen inspired greedy chain extraction to place
4-
//! hot paths contiguously and avoid cache line straddling.
3+
//! Extracts linear chains from the control flow graph and places them
4+
//! contiguously. Pads instructions to prevent cache line straddling.
55
66
use std::collections::{BTreeMap, HashSet};
77

@@ -161,18 +161,23 @@ fn assign_step_ids(
161161
};
162162
let size = instr.size();
163163

164-
// Cache line alignment for large instructions
165-
if size >= 48 {
166-
let line_offset = current_offset % CACHE_LINE;
167-
if line_offset + size > CACHE_LINE {
168-
// Would straddle cache line - pad to next line
169-
let padding_bytes = CACHE_LINE - line_offset;
170-
let padding_steps = (padding_bytes / STEP_SIZE) as u16;
171-
current_step += padding_steps;
172-
current_offset += padding_bytes;
173-
}
164+
// Pad if instruction would straddle cache line boundary
165+
let line_offset = current_offset % CACHE_LINE;
166+
if line_offset + size > CACHE_LINE {
167+
let padding_bytes = CACHE_LINE - line_offset;
168+
let padding_steps = (padding_bytes / STEP_SIZE) as u16;
169+
current_step += padding_steps;
170+
current_offset += padding_bytes;
174171
}
175172

173+
// Invariant: instruction must not straddle cache line
174+
assert!(
175+
current_offset % CACHE_LINE + size <= CACHE_LINE,
176+
"instruction at offset {} with size {} straddles 64-byte cache line",
177+
current_offset,
178+
size
179+
);
180+
176181
mapping.insert(label, current_step);
177182
let step_count = (size / STEP_SIZE) as u16;
178183
current_step += step_count;

crates/plotnik-lib/src/emit/layout_tests.rs

Lines changed: 123 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -113,45 +113,132 @@ fn layout_branch() {
113113
}
114114

115115
#[test]
116-
fn layout_large_instruction_cache_alignment() {
117-
// Large instruction (Match48 = 48 bytes = 6 steps) near cache line boundary
118-
// Start at step 5 (offset 40), would straddle - should pad
119-
let large_match = MatchIR::at(Label(1))
120-
.nav(Nav::Down)
121-
.node_type(NodeTypeIR::Named(NonZeroU16::new(10)))
122-
.pre_effect(EffectIR::start_obj())
123-
.pre_effect(EffectIR::start_obj())
124-
.pre_effect(EffectIR::start_obj())
125-
.post_effect(EffectIR::node())
126-
.post_effect(EffectIR::end_obj())
127-
.post_effect(EffectIR::end_obj())
128-
.post_effect(EffectIR::end_obj())
129-
.next_many(vec![
130-
Label(100),
131-
Label(101),
132-
Label(102),
133-
Label(103),
134-
Label(104),
135-
Label(105),
136-
Label(106),
137-
Label(107),
138-
]);
139-
140-
// Verify it's large enough to trigger alignment
141-
assert!(large_match.size() >= 48);
116+
fn layout_match16_cache_alignment() {
117+
// Match16 (16 bytes, 2 steps) at offset 56 would straddle (56+16=72 > 64)
118+
// Place 7 Match8s (7 steps = 56 bytes) before Match16
119+
// Expected: Match16 gets padded to step 8 (offset 64)
120+
let mut instructions = Vec::new();
121+
122+
// 7 Match8 instructions in a chain: Label(0) -> Label(1) -> ... -> Label(6) -> Label(7)
123+
for i in 0..7 {
124+
instructions.push(
125+
MatchIR::at(Label(i))
126+
.nav(Nav::Down)
127+
.next(Label(i + 1))
128+
.into(),
129+
);
130+
}
131+
132+
// Match16 at Label(7): needs 2+ successors to become Match16
133+
instructions.push(
134+
MatchIR::at(Label(7))
135+
.nav(Nav::Down)
136+
.next_many(vec![Label(100), Label(101)])
137+
.into(),
138+
);
142139

143-
let instructions = vec![
144-
// Small instruction first
145-
MatchIR::epsilon(Label(0), Label(1)).into(),
146-
large_match.into(),
147-
];
140+
let result = CacheAligned::layout(&instructions, &[Label(0)]);
141+
142+
// Labels 0-6 should be at steps 0-6 (no padding needed)
143+
for i in 0..7 {
144+
assert_eq!(
145+
result.label_to_step.get(&Label(i)),
146+
Some(&(i as u16)),
147+
"Label({i}) should be at step {i}"
148+
);
149+
}
150+
151+
// Label(7) would be at step 7 (offset 56) without padding
152+
// But Match16 at offset 56 straddles (56+16=72 > 64), so it must be padded
153+
// After padding: step 8 (offset 64)
154+
let step7 = *result.label_to_step.get(&Label(7)).unwrap();
155+
assert_eq!(step7, 8, "Match16 should be padded to step 8 (offset 64)");
156+
157+
// Total steps: 8 (padding at step 7) + 2 (Match16) = 10
158+
assert_eq!(result.total_steps, 10);
159+
}
160+
161+
#[test]
162+
fn layout_match8_no_padding_needed() {
163+
// Match8 (8 bytes) never straddles: max offset 56, 56+8=64 <= 64
164+
// Place 7 Match8s, then another Match8 - should NOT need padding
165+
let mut instructions = Vec::new();
166+
167+
for i in 0..8 {
168+
if i < 7 {
169+
instructions.push(
170+
MatchIR::at(Label(i))
171+
.nav(Nav::Down)
172+
.next(Label(i + 1))
173+
.into(),
174+
);
175+
} else {
176+
instructions.push(MatchIR::terminal(Label(i)).nav(Nav::Down).into());
177+
}
178+
}
148179

149180
let result = CacheAligned::layout(&instructions, &[Label(0)]);
150181

151-
// Label 0 at step 0 (offset 0)
152-
assert_eq!(result.label_to_step.get(&Label(0)), Some(&0u16));
182+
// All 8 Match8s should be contiguous: steps 0-7
183+
for i in 0..8 {
184+
assert_eq!(
185+
result.label_to_step.get(&Label(i)),
186+
Some(&(i as u16)),
187+
"Label({i}) should be at step {i} (no padding)"
188+
);
189+
}
190+
191+
// Total steps: 8 (no padding)
192+
assert_eq!(result.total_steps, 8);
193+
}
194+
195+
#[test]
196+
fn layout_match32_cache_alignment() {
197+
// Match32 (32 bytes, 4 steps) at offset 40 would straddle (40+32=72 > 64)
198+
// Place 5 Match8s (5 steps = 40 bytes) before Match32
199+
// Expected: Match32 gets padded to step 8 (offset 64)
200+
let mut instructions: Vec<crate::bytecode::InstructionIR> = Vec::new();
201+
202+
// 5 Match8 instructions: Label(0) -> ... -> Label(4) -> Label(5)
203+
for i in 0..5 {
204+
instructions.push(
205+
MatchIR::at(Label(i))
206+
.nav(Nav::Down)
207+
.next(Label(i + 1))
208+
.into(),
209+
);
210+
}
211+
212+
// Match32 at Label(5): needs enough payload to become Match32 (9-12 slots)
213+
// 3 pre + 3 post + 4 successors = 10 slots -> Match32
214+
instructions.push(
215+
MatchIR::at(Label(5))
216+
.nav(Nav::Down)
217+
.pre_effect(EffectIR::start_obj())
218+
.pre_effect(EffectIR::start_obj())
219+
.pre_effect(EffectIR::start_obj())
220+
.post_effect(EffectIR::end_obj())
221+
.post_effect(EffectIR::end_obj())
222+
.post_effect(EffectIR::end_obj())
223+
.next_many(vec![Label(100), Label(101), Label(102), Label(103)])
224+
.into(),
225+
);
226+
227+
// Verify it's Match32 (32 bytes)
228+
assert_eq!(instructions.last().unwrap().size(), 32);
229+
230+
let result = CacheAligned::layout(&instructions, &[Label(0)]);
231+
232+
// Labels 0-4 at steps 0-4
233+
for i in 0..5 {
234+
assert_eq!(result.label_to_step.get(&Label(i)), Some(&(i as u16)));
235+
}
236+
237+
// Label(5) would be at step 5 (offset 40) without padding
238+
// Match32 at offset 40 straddles (40+32=72 > 64), so padded to step 8
239+
let step5 = *result.label_to_step.get(&Label(5)).unwrap();
240+
assert_eq!(step5, 8, "Match32 should be padded to step 8 (offset 64)");
153241

154-
// Label 1 should be aligned - either at step 1 or padded to cache line
155-
let step1 = *result.label_to_step.get(&Label(1)).unwrap();
156-
assert!(step1 >= 1);
242+
// Total steps: 8 (3 padding steps at 5,6,7) + 4 (Match32) = 12
243+
assert_eq!(result.total_steps, 12);
157244
}

crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__emit_tests__alternations_captured.snap

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ _ObjWrap:
3434
05 ▶
3535

3636
Test:
37-
06 ε 07
38-
07 ε 10, 12
39-
09 ▶
40-
10 ! (identifier) [Node Set(M0)] 09
41-
12 ! (number) [Node Set(M0)] 09
37+
06 ε 08
38+
07 ...
39+
08 ε 11, 13
40+
10 ▶
41+
11 ! (identifier) [Node Set(M0)] 10
42+
13 ! (number) [Node Set(M0)] 10

crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__emit_tests__alternations_captured_tagged.snap

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ _ObjWrap:
4545
05 ▶
4646

4747
Test:
48-
06 ε 07
49-
07 ε 12, 15
50-
09 ▶
51-
10 ε [Set(M4)] 09
52-
12 ! [Enum(M2)] (identifier) [Node Set(M0) EndEnum] 10
53-
15 ! [Enum(M3)] (number) [Node Set(M1) EndEnum] 10
48+
06 ε 08
49+
07 ...
50+
08 ε 13, 16
51+
10 ▶
52+
11 ε [Set(M4)] 10
53+
13 ! [Enum(M2)] (identifier) [Node Set(M0) EndEnum] 11
54+
16 ! [Enum(M3)] (number) [Node Set(M1) EndEnum] 11

crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__emit_tests__alternations_in_quantifier.snap

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -53,23 +53,26 @@ Test:
5353
06 ε 07
5454
07 ! (object) 08
5555
08 ε [Arr] 10
56-
10 ε 39, 20
56+
10 ε 42, 21
5757
12 ε [EndArr Set(M5)] 14
58-
14 △ _ 19
59-
15 ε [EndObj Push] 17
60-
17 ε 45, 12
61-
19 ▶
62-
20 ε [EndArr Set(M5)] 19
63-
22 ε [Set(M4)] 15
64-
24 ! [Enum(M2)] (pair) [Node Set(M0) EndEnum] 22
65-
27 ! [Enum(M3)] (shorthand_property_identifier) [Node Set(M1) EndEnum] 22
66-
30 ε 24, 27
67-
32 ε [Obj] 30
68-
34 ▷ _ 37
69-
35 ε 34, 12
70-
37 ε 32, 35
71-
39 ▽ _ 37
72-
40 ▷ _ 43
73-
41 ε 40, 12
74-
43 ε 32, 41
75-
45 ▷ _ 43
58+
14 △ _ 20
59+
15 ...
60+
16 ε [EndObj Push] 18
61+
18 ε 48, 12
62+
20 ▶
63+
21 ε [EndArr Set(M5)] 20
64+
23 ...
65+
24 ε [Set(M4)] 16
66+
26 ! [Enum(M2)] (pair) [Node Set(M0) EndEnum] 24
67+
29 ! [Enum(M3)] (shorthand_property_identifier) [Node Set(M1) EndEnum] 24
68+
32 ε 26, 29
69+
34 ε [Obj] 32
70+
36 ▷ _ 40
71+
37 ε 36, 12
72+
39 ...
73+
40 ε 34, 37
74+
42 ▽ _ 40
75+
43 ▷ _ 46
76+
44 ε 43, 12
77+
46 ε 34, 44
78+
48 ▷ _ 46

crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__emit_tests__alternations_labeled.snap

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ _ObjWrap:
4545
05 ▶
4646

4747
Test:
48-
06 ε 07
49-
07 ε 10, 13
50-
09 ▶
51-
10 ! [Enum(M2)] (identifier) [Node Set(M0) EndEnum] 09
52-
13 ! [Enum(M3)] (number) [Node Set(M1) EndEnum] 09
48+
06 ε 08
49+
07 ...
50+
08 ε 11, 16
51+
10 ▶
52+
11 ! [Enum(M2)] (identifier) [Node Set(M0) EndEnum] 10
53+
14 ...
54+
15 ...
55+
16 ! [Enum(M3)] (number) [Node Set(M1) EndEnum] 10

0 commit comments

Comments
 (0)