graph-clonable-ref 0.2.0

A fast, thread-safe cloneable reference that preserves reference structure during deep cloning
Documentation

graph-clonable-ref

Crates.io Documentation License: MIT CI

A fast, thread-safe cloneable reference type for Rust that preserves reference structure during deep cloning.

The Problem

When you have shared references (like Arc<RwLock<T>>) in a struct and clone it, each reference is cloned independently. References that pointed to the same data before cloning now point to different copies — structure is lost.

Traditional solutions use a HashMap<*const T, Arc<T>> during cloning to track which references have been cloned. This works but is slow (~17ns per reference lookup).

The Solution

This crate uses generation-based tracking instead of HashMap lookups:

  • Each RefGraph caches its clone with a globally unique generation number
  • During deep_clone(), a unique generation is assigned via atomic counter
  • References check if their graph's cached clone matches the current generation
  • First reference triggers the clone, subsequent references get O(1) cache hits
  • Cache entries use Weak refs and auto-expire when no longer referenced

Result: ~7 nanoseconds per reference — thread-safe, panic-safe, and 2.5x faster than HashMap.

Features

  • Thread-safeSend + Sync, works with Arc, actix-web, tokio, rayon, etc.
  • Panic-safeDeepCloneGuard with depth tracking resets state correctly on unwind
  • Fast — generation-based O(1) cache hits, ~7ns per reference
  • Nestabledeep_clone inside deep_clone works correctly
  • Zero boilerplate — works with #[derive(Clone)], no special traits needed
  • No 'static bound — works with any lifetime

Installation

Add this to your Cargo.toml:

[dependencies]
graph-clonable-ref = "0.1"

Usage

Basic Example

use graph_clonable_ref::{RefGraph, GraphRef, deep_clone};

// Create a graph (container for related references)
let graph = RefGraph::new();

// Create references within the graph
let a = graph.create(42);
let b = a.clone();  // b points to same data as a

// Verify they share data
a.set(100);
assert_eq!(b.get(), 100);

// Deep clone preserves structure
let (a2, b2) = deep_clone(&(a.clone(), b.clone()));

// a2 and b2 point to the SAME new data
a2.set(999);
assert_eq!(b2.get(), 999);

// Original is unaffected
assert_eq!(a.get(), 100);

In Structs

use graph_clonable_ref::{RefGraph, GraphRef, deep_clone};

#[derive(Clone)]
struct NeuralNetwork {
    weights: Vec<GraphRef<f64>>,
    biases: Vec<GraphRef<f64>>,
    tied_weights: Vec<GraphRef<f64>>,  // shares data with weights
}

impl NeuralNetwork {
    fn new() -> Self {
        let graph = RefGraph::new();
        let weights: Vec<_> = (0..100).map(|_| graph.create(0.0)).collect();
        let biases: Vec<_> = (0..10).map(|_| graph.create(0.0)).collect();
        let tied_weights = weights[0..10].to_vec();
        NeuralNetwork { weights, biases, tied_weights }
    }
}

let net = NeuralNetwork::new();
let net2 = deep_clone(&net);

// net2's tied_weights still reference the same data as net2's weights
// but are independent from net's weights

Thread-Safe Usage (actix-web)

use actix_web::{web, App, HttpServer, HttpResponse};
use graph_clonable_ref::{RefGraph, GraphRef, deep_clone};

#[derive(Clone)]
struct AppState {
    weights: Vec<GraphRef<f64>>,
    tied_weights: Vec<GraphRef<f64>>,
}

async fn train_handler(data: web::Data<AppState>) -> HttpResponse {
    // Each request gets its own independent copy
    let mut local = deep_clone(data.get_ref());

    local.weights[0].set(1.0);
    assert_eq!(local.tied_weights[0].get(), 1.0); // still tied

    HttpResponse::Ok().finish()
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let graph = RefGraph::new();
    let weights: Vec<_> = (0..100).map(|i| graph.create(i as f64)).collect();
    let tied_weights = weights[..10].to_vec();

    let state = web::Data::new(AppState { weights, tied_weights });

    HttpServer::new(move || {
        App::new()
            .app_data(state.clone())
            .route("/train", web::post().to(train_handler))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Multiple Graphs

References from different graphs remain independent:

use graph_clonable_ref::{RefGraph, GraphRef, deep_clone};

let graph1 = RefGraph::new();
let graph2 = RefGraph::new();

let a = graph1.create(1);
let b = graph1.create(2);
let c = graph2.create(3);

let (a2, b2, c2) = deep_clone(&(a.clone(), b.clone(), c.clone()));

// a2 and b2 share the same cloned graph
assert!(a2.same_graph(&b2));

// c2 is in a different cloned graph
assert!(!a2.same_graph(&c2));

API Reference

RefGraph<T: Send + Sync>

Container for a group of related references.

let graph = RefGraph::new();              // -> Arc<RefGraph<T>>
let r: GraphRef<i32> = graph.create(42);  // create a reference
let count = graph.len();                  // number of values
graph.clear_cache();                      // free clone cache memory

GraphRef<T: Send + Sync>

A cloneable reference that preserves structure during deep cloning.

let value = r.get();          // get value (requires T: Clone)
r.set(100);                   // set value
r.update(|v| *v += 1);       // update with function
a.ptr_eq(&b);                // same graph and index?
a.same_graph(&b);            // same graph?
r.index();                   // index within graph

deep_clone<T: Clone>(value: &T) -> T

Performs a structure-preserving deep clone. Thread-safe and panic-safe.

begin_deep_clone() -> DeepCloneGuard

For manual control over the cloning scope (advanced). Supports nesting.

let _guard = begin_deep_clone();
let a2 = a.clone();  // deep clone
let b2 = b.clone();  // deep clone, same generation
// guard dropped -> back to shallow clone behavior

Performance

Benchmarks on a network with 5 layers x 500 references (3750 total with sharing):

Approach Time Per Reference
Generation-based (this crate) ~26 µs ~7 ns
HashMap-based ~64 µs ~17 ns
Two-phase index (clone only) ~105 µs ~28 ns
Two-phase index (with prepare) ~191 µs ~51 ns

Run benchmarks yourself:

cargo bench

How It Works

  1. Normal clone (a.clone()): Returns a new GraphRef pointing to the same data (shallow, like Arc::clone)

  2. Deep clone (deep_clone(&x)):

    • Assigns a globally unique generation via AtomicU64
    • Calls x.clone() which triggers special behavior
    • Each GraphRef::clone() checks: does my graph have a cached clone for this generation?
      • Yes (read lock): Return a ref to the cached clone — O(1)
      • No (write lock): Clone the graph, cache with Weak ref, return new ref
    • DeepCloneGuard resets state on drop (even on panic)

Cache entries use Weak<RefGraph<T>> so they auto-expire when the cloned graph is dropped. Dead entries are cleaned up lazily on cache-miss writes.

License

MIT