Avoiding RefCells in compute graphs


#1

I’m trying to build a compute graph where some of the nodes in the graph need to store some internal state that they can modify on each render. Think caching, or an internal render counter. After trying a few approaches I discovered I could use RefCell for this.

To simplify things my compute node called Op takes a single input and returns a single output:

trait Op {
    fn name(&self) -> &str;
    fn render(&self, input: i32) -> i32;
}

Ops are tied together with Connections, which are just the names of the Ops:

#[derive(Clone)]
struct Connection {
    output: String,
    input: String,
}

They are stored in a Network which is just a container of Op trait objects and Connections:

struct Network {
    ops: Vec<Box<Op>>,
    connections: Vec<Connection>,
}

The “frame” op is an example of an operation that uses a RefCell to change its inner contents:

struct FrameOp {
    name: String,
    frame: RefCell<i32>,
}

impl Op for FrameOp {
    fn name(&self) -> &str {
        &self.name
    }
    fn render(&self, _input: i32) -> i32 {
        let mut frame = self.frame.borrow_mut();
        *frame += 1;
        *frame
    }
}

My question is, is this an idiomatic way of using Rust or is there a better way that would avoid the runtime cost of RefCells? I’ve tried making render take a mutable reference but got in a fight with the borrow checker.

Here’s a Rust Playground showing the full example.


#2

You can use a Cell instead of RefCell in this particular example, and that won’t have a dynamic borrow check.

In general, you probably can restructure the code to use mutable borrows, but I’m not sure if it’ll be better in the end.


#3

At least from what you posted, I don’t see a reason you couldn’t use normal mutable borrows. The determining factor is where you get your reference to the Op from. In your example you have a vector of owned Ops, so you can just borrow any of them and mutate them as you like. RefCells are necessary when you have shared ownership.


#4

Thanks for your answers!

I would prefer getting this working with normal mutable borrows. The issue I got stuck with is that I’m borrowing things more than once in the Network::render method. Rendering a single node works fine, but I couldn’t get it working when doing it recursively.

Ideally the render method would look like this (but Rust can’t make the guarantees it needs in this form):

fn render(&mut self, op_name: &str) -> i32 {
    // Find the op to render
    let mut op = self.find_op_mut(&op_name);
    // Provide a default value for its input
    let mut input = 42;

    // Render its input (recursive)
    for connection in &self.connections {
        if connection.input == op.name() {
            let mut out_op = self.find_op_mut(&connection.output);
            input = self.render(out_op.name());
            break;
        }
    }

    // Render the op itself
    op.render(input)
}

Playground:
Non-working version with mutable borrows


#5

Here is a sketch of one option, but you be the judge of whether it’s better :slight_smile:.


#6

Thanks! Working with indices instead of references is a great idea. I’ll probably take this a bit further and see if I can make a mapping between internal ID’s and Ops; this would make lookup faster as well.


#7

Can’t claim credit for the idea - that’s pretty much the canonical way to work around aliasing and borrowing issues in non-linear borrowing scenarios.

The downside, of course, is the indices are subject to staleness - i.e. if a modification is made somewhere, but the index isn’t updated, you’ll have a bug that may be tricky to track down. References prevent this statically but of course bring their own caveats to the table.