Skip to content

Commit 0b462d6

Browse files
authored
Merge pull request #8 from bluedynamics/optimize/lazy-memo-sync
Optimize memo sync: lazy dirty flags instead of eager cloning
2 parents 819b21b + de928a6 commit 0b462d6

1 file changed

Lines changed: 85 additions & 24 deletions

File tree

src/decode.rs

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ struct Decoder<'a> {
3737
metastack: Vec<Vec<PickleValue>>,
3838
/// Tracks which memo indices are bound to each stack slot (parallel to stack).
3939
/// When BINPUT stores from stack top, the memo index is recorded here.
40-
/// After in-place mutations (SETITEMS, APPENDS, etc.), stale memo entries
41-
/// are updated via sync_memo_top().
4240
stack_memo: Vec<Vec<usize>>,
4341
/// Saved stack_memo during MARK (parallel to metastack).
4442
meta_stack_memo: Vec<Vec<Vec<usize>>>,
43+
/// Dirty flags parallel to memo: true means the memo entry is stale
44+
/// (the owning stack slot was mutated after BINPUT stored the value).
45+
/// Resolved lazily at BINGET or eagerly when the slot is popped.
46+
dirty_memo: Vec<bool>,
4547
}
4648

4749
impl<'a> Decoder<'a> {
@@ -54,6 +56,7 @@ impl<'a> Decoder<'a> {
5456
metastack: Vec::with_capacity(4),
5557
stack_memo: Vec::with_capacity(16),
5658
meta_stack_memo: Vec::with_capacity(4),
59+
dirty_memo: Vec::with_capacity(16),
5760
}
5861
}
5962

@@ -310,7 +313,7 @@ impl<'a> Decoder<'a> {
310313
));
311314
}
312315
}
313-
self.sync_memo_top()?;
316+
self.mark_top_dirty();
314317
}
315318
APPENDS => {
316319
let items = self.pop_mark()?;
@@ -335,7 +338,7 @@ impl<'a> Decoder<'a> {
335338
));
336339
}
337340
}
338-
self.sync_memo_top()?;
341+
self.mark_top_dirty();
339342
}
340343

341344
// -- Dict --
@@ -369,7 +372,7 @@ impl<'a> Decoder<'a> {
369372
));
370373
}
371374
}
372-
self.sync_memo_top()?;
375+
self.mark_top_dirty();
373376
}
374377
SETITEMS => {
375378
let items = self.pop_mark()?;
@@ -395,7 +398,7 @@ impl<'a> Decoder<'a> {
395398
));
396399
}
397400
}
398-
self.sync_memo_top()?;
401+
self.mark_top_dirty();
399402
}
400403

401404
// -- Set/FrozenSet (protocol 4) --
@@ -410,7 +413,7 @@ impl<'a> Decoder<'a> {
410413
"ADDITEMS on non-set".to_string(),
411414
));
412415
}
413-
self.sync_memo_top()?;
416+
self.mark_top_dirty();
414417
}
415418
FROZENSET => {
416419
let items = self.pop_mark()?;
@@ -609,7 +612,7 @@ impl<'a> Decoder<'a> {
609612
if let Some(top_bindings) = self.stack_memo.last_mut() {
610613
top_bindings.extend(obj_bindings);
611614
}
612-
self.sync_memo_top()?;
615+
self.mark_top_dirty();
613616
}
614617
NEWOBJ => {
615618
let args = self.pop_value()?;
@@ -783,8 +786,29 @@ impl<'a> Decoder<'a> {
783786

784787
#[inline]
785788
fn pop_value(&mut self) -> Result<PickleValue, CodecError> {
786-
self.stack_memo.pop();
787-
self.stack.pop().ok_or(CodecError::StackUnderflow)
789+
let bindings = self.stack_memo.pop().unwrap_or_default();
790+
let val = self.stack.pop().ok_or(CodecError::StackUnderflow)?;
791+
// Sync any dirty memo entries before the value leaves the stack
792+
if !bindings.is_empty() {
793+
let mut need_clone = false;
794+
for &idx in &bindings {
795+
if idx < self.dirty_memo.len() && self.dirty_memo[idx] {
796+
need_clone = true;
797+
break;
798+
}
799+
}
800+
if need_clone {
801+
for &idx in &bindings {
802+
if idx < self.dirty_memo.len() && self.dirty_memo[idx] {
803+
if idx < self.memo.len() {
804+
self.memo[idx] = val.clone();
805+
}
806+
self.dirty_memo[idx] = false;
807+
}
808+
}
809+
}
810+
}
811+
Ok(val)
788812
}
789813

790814
#[inline]
@@ -802,9 +826,19 @@ impl<'a> Decoder<'a> {
802826
// Take the current stack (everything since MARK) as the result.
803827
// This is a pointer swap — no element-by-element drain needed.
804828
let items = std::mem::take(&mut self.stack);
829+
let slot_memos = std::mem::take(&mut self.stack_memo);
805830

806-
// Also discard the memo bindings for popped items
807-
self.stack_memo.clear();
831+
// Sync dirty memo entries for all popped slots before values are consumed
832+
for (val, bindings) in items.iter().zip(slot_memos.iter()) {
833+
for &idx in bindings {
834+
if idx < self.dirty_memo.len() && self.dirty_memo[idx] {
835+
if idx < self.memo.len() {
836+
self.memo[idx] = val.clone();
837+
}
838+
self.dirty_memo[idx] = false;
839+
}
840+
}
841+
}
808842

809843
// Restore the previous stack from metastack
810844
if let Some(old_stack) = self.metastack.pop() {
@@ -825,18 +859,48 @@ impl<'a> Decoder<'a> {
825859
}
826860
if idx >= self.memo.len() {
827861
self.memo.resize(idx + 1, PickleValue::None);
862+
self.dirty_memo.resize(idx + 1, false);
828863
}
829864
self.memo[idx] = val;
865+
self.dirty_memo[idx] = false;
830866
Ok(())
831867
}
832868

833-
fn memo_get(&self, idx: usize) -> Result<PickleValue, CodecError> {
869+
/// Get a memo entry, lazily resolving dirty (stale) entries first.
870+
fn memo_get(&mut self, idx: usize) -> Result<PickleValue, CodecError> {
871+
if idx < self.dirty_memo.len() && self.dirty_memo[idx] {
872+
self.resolve_dirty_memo(idx);
873+
}
834874
self.memo
835875
.get(idx)
836876
.cloned()
837877
.ok_or_else(|| CodecError::InvalidData(format!("memo index {idx} not found")))
838878
}
839879

880+
/// Resolve a dirty memo entry by finding its live value on the stack.
881+
fn resolve_dirty_memo(&mut self, memo_idx: usize) {
882+
// Search current stack for the slot that owns this memo binding
883+
for (si, bindings) in self.stack_memo.iter().enumerate() {
884+
if bindings.contains(&memo_idx) {
885+
self.memo[memo_idx] = self.stack[si].clone();
886+
self.dirty_memo[memo_idx] = false;
887+
return;
888+
}
889+
}
890+
// Search metastack (values saved by MARK)
891+
for (mi, meta_sm) in self.meta_stack_memo.iter().enumerate() {
892+
for (si, bindings) in meta_sm.iter().enumerate() {
893+
if bindings.contains(&memo_idx) {
894+
self.memo[memo_idx] = self.metastack[mi][si].clone();
895+
self.dirty_memo[memo_idx] = false;
896+
return;
897+
}
898+
}
899+
}
900+
// Value was already consumed from stack — memo has the last stored value
901+
self.dirty_memo[memo_idx] = false;
902+
}
903+
840904
/// Record that the current stack top was stored in memo at `idx`.
841905
#[inline]
842906
fn record_memo_binding(&mut self, idx: usize) {
@@ -845,21 +909,18 @@ impl<'a> Decoder<'a> {
845909
}
846910
}
847911

848-
/// After an in-place mutation of the stack top (SETITEMS, APPENDS, etc.),
849-
/// update any memo entries that were cloned from the pre-mutation state.
850-
fn sync_memo_top(&mut self) -> Result<(), CodecError> {
912+
/// Mark memo entries bound to the current stack top as dirty (stale).
913+
/// Called after in-place mutations (SETITEMS, APPENDS, etc.) instead of
914+
/// eagerly cloning. Resolution is deferred to memo_get() or pop_value().
915+
#[inline]
916+
fn mark_top_dirty(&mut self) {
851917
if let Some(bindings) = self.stack_memo.last() {
852-
if !bindings.is_empty() {
853-
let new_val = self.stack.last()
854-
.ok_or(CodecError::StackUnderflow)?.clone();
855-
for &idx in bindings {
856-
if idx < self.memo.len() {
857-
self.memo[idx] = new_val.clone();
858-
}
918+
for &idx in bindings {
919+
if idx < self.dirty_memo.len() {
920+
self.dirty_memo[idx] = true;
859921
}
860922
}
861923
}
862-
Ok(())
863924
}
864925
}
865926

0 commit comments

Comments
 (0)