[Small side-note section describing the context; skip to the next paragraph for the actual issue.]
Yesterday I stumbled upon an "horrible" bug in my code that triggered stack overflows, and no matter what I tried to pin-point it made it seem that the issue lied elsewhere than where I was looking for it... Moreover it seemed that the stack overflow happen after any piece of code...
It turns out that the issue was caused by a recursive drop
call in a linked-list-like structure based on Rc
.
The problem
Given a structure similar to the following:
struct Node <V : Default> (Rc<NodeData<V>>);
struct NodeData <V : Default> {
value : V,
next : Option<Node<V>>,
}
The default ops::Drop
implementation would recurse through the linked structure, thus eventually triggering an stack overflow abort. (This happens for lists longer than ~40k elements.)
Therefore one should implement an alternative ops::Drop
that would elide this issue.
The question
How would one efficiently implement an "iterative drop", that eluded the recursive calls?
The proposed solution
My take on this is given in the following Rust playgroud snippet:
(One can use DROP_ITERATIVE
to enable or disable the functionality.)
Which features the following ops::Drop
implementation:
impl <V : Default> ops::Drop for Node<V> {
fn drop (&mut self) -> () {
loop {
// We get `Some(data)` if the RC's strong count is exactly 1, thus it will be dropped.
let next = if let Some (data) = Rc::get_mut (&mut self.0) {
// We try to extract the next node...
// In doying so we need to swap the node internal data with some "dummy" one.
let mut swap = NodeData { value : Default::default (), next : None };
mem::swap (&mut swap, data);
if let Some (next) = swap.next {
next
} else {
return;
}
} else {
// The underlying RC is used elsewhere, thus it won't be dropped.
return;
};
// Behind the following assignment the following happens:
// (1) `drop` will be called once more for the current value;
// (2) `self` will be replaced with `next`, thus "eating" from the list one more step;
*self = next;
}
}
}
The issue with the proposed solution
Unfortunately it seems that my implementation is not quite that efficient...
Trying to time the drop in the playground example yields comparable times for with and without the iterative drop, thus it suggests the implementation is quite efficient.
However when I plug it into my actual code (a Scheme interpreter, where the linked-list-like Node
is actually part of a generic Value
object) the efficiency drops almost 10 times...
The following are the links to my actual implementation:
-
how the
Node
structure is implemented (it is calledPairImmutable
) (it is very similar with the playground version, but instead uses a genericValue
asnext
which in turn is anenum
that has other nodes or non-nodes variants): -
how the
Node
implements iterative drop (thetry_into
function tries to convert a genericValue
object into anNode
value): -
how the
Node
object (calledPairImmutable
) plugs into the genericValue
:
Other alternative implementations?
Has anyone stumbled upon a similar issue before? Has anyone seen alternative implementations?
Someone on IRC pointed me to Rust's LinkedList
which implements such an iterative drop technique, however as LinkedList
doesn't use Rc
, but instead manual memory management, it doesn't seem to be directly applicable to the usecase of Rc
-based linked structure.
Thanks for the help,
Ciprian.