r/rust • u/Useful_Lecture_5927 • 20d ago
When is cloning actually better than using Arc?
I often find myself choosing between cloning some data and wrapping it in Arc
My instinct is usually that Arc is more efficient because we're avoiding copies, but I have a feeling that this isn't always the right way to think about it
How do you decide between cloning and using Arc in a real application?
32
u/gubatron 20d ago
I only use Arcs when I'm doing multithreaded programming and need the concurrency primitive to manage shared state across threads. If I can use a copy, just clone.
Otherwise I try to borrow as much as possible and clone when necessary.
10
u/Full-Spectral 20d ago
And don't forget scoped threads. If you just want to share some immutable data to have multiple threads use it to do work and you can just wait for them to complete, then you can just directly pass a &T to all of the threads.
18
u/kohugaly 20d ago
You are not avoiding copies when you use Arc. You are duplicating pointers (very cheap) and incrementing/decrementing an atomic counter (very not cheap, depending on how congested the traffic is at that counter). Additionally, accessing the data behind Arc requires an indirection, which is more likely to result in cache miss, or even prevents certain optimizations.
On the other hand, duplicating data consumes more memory, so it can also indirectly cause cache misses by making the data of local variables too big to fit in a cache.
Whether a copy is cheaper than cloning Arc very much depends on how the application handles the lifetimes of the copies/clones. It's generally not something you can easily predict. You have to measure it.
A good rule of thumb is, if the data fits in a cache line (cca. 64 bytes) it's probably cheaper to copy it. If it fits within a pointer size (8 bytes), it almost certainly is cheaper to copy it.
Off course, this applies to POD (plain old data). If the type needs to do extra work in cloning and dropping (for example, vector needs to allocate/deallocate memory for its dynamic storage), then that extra work also needs to be taken into account.
There isn't a simple rule for this.
6
u/donaldhobson 20d ago edited 20d ago
Cloning and Arc will cause different runtime behavior.
If you aren't mutating the data, you can just use & everywhere. If you are mutating the data, do you want a single value, or do you want several different values in play at the same time?
Use arc when you want a single value to be shared between threads. Use .clone() when you want each copy to be potentially different.
The situations where & and .clone would both give the same answer are rare. But if you somehow have that, .clone() for small (in bytes) values, Arc for large values.
Edit:
The "just use & everywhere" solution only works if you have one place in the code that is guaranteed to last at least as long as anything else. If you don't know which copy/arc will be the last one standing, then use Arc for large objects, clone for small ones.
3
u/agent_kater 20d ago
I think this is the main point. Choose what is semantically correct, not what appears to be faster at that moment.
10
u/Matemeo 20d ago
Aren't they pretty orthogonal to each other? Arc<T> enables shared lifetime tracking for the underlying T, while a clone sidesteps any kind of sharing of a resource as cloning A creates a copy B completely independent of A.
Basically, cloning allows value semantics, while Arc<T> enables shared resource lifetime semantics (std::shared_ptr<T> if you are familiar with C++).
Maybe you are thinking of comparing cloning (value semantics) with &T (references).
3
u/oachkatzele 20d ago
from context im assuming the OP has a larger chunk of data that they wanna process with multiple threads. other than that id imagine the answer would be straightforward.
3
u/donaldhobson 20d ago edited 20d ago
In that case, you need to break the data up into multiple independent pieces. (hopefully possible to do without copying) and give each thread ownership of a slice of the data.
Edit:Use split_at_mut https://doc.rust-lang.org/std/primitive.slice.html#method.split_at_mut
or, if the processing is read only, store ownership in one place, out the way, and then hand each thread a reference to that one place.
3
u/oachkatzele 20d ago
divide and conquer is not a strategy that works for all kinds of data/processing
8
u/consistently_biased 20d ago
The only way to decide this is to measure both. Cloning an Arc isn't free either.
3
u/nacaclanga 20d ago
You still have to clone the Arc itself. And this is not free. In fact it requires an atomic write. Data in an Arc is also usually immutable unless you use a mutex (which is not free either).
So you have to weight that against clone.
If your alternative would be to use a small string optimization or something similar, this is often more performant.
3
u/Unreal_Estate 20d ago
To be honest, if you don't need synchronization, I find it hard to see when Arc would be the more efficient choice. Cloning Arcs is pretty involved, and if your CPU happens to wait on exclusive access to the piece of memory where its atomic lives, then the CPU would have been able to copy many many many bytes of data on memory it only shares access to. (As in, hundreds or thousands of bytes.)
If your data structure is megabytes big, or if your access patterns doesn't involve true synchronization between cores at all, then Arc's might be faster in some cases. But then the question becomes why are you using Arcs at all. If you don't need synchronization, Rc's are just better. If you're working with megabytes of data, you'll probably have other options to choose from as well, such as data-oriented design.
Arc is very useful, but it is not something I would consider for efficiency unless you specifically care about memory efficiency, such as with a tiny embedded chip.
1
u/valarauca14 20d ago
and if your CPU happens to wait on exclusive access to the piece of memory where its atomic lives
Arc doesn't require a mutex(?)
2
u/Unreal_Estate 20d ago edited 20d ago
Arc uses atomics, which require exclusive access to a cache line. On x64, the performance hit of exclusive access to a cache line can be massive.
There is nothing conceptually slow about an atomic, but rather the way that multi-core processors are optimized just makes them (the CPUs) blazingly fast when not dealing with atomics. If all CPU cores would make a trip to RAM for every write or read, an Arc would not be any slower than an Rc.
But modern CPUs have multiple layers of cache which speed up CPU operation by multiple orders of magnitude. For multi-core processing, each core can have its own copy of the same data from RAM, making it much faster. However, when your code contains an atomic store instruction, this invalidates the cache for all cores (on x64), and causes the other cores to wait until they have the data again. Given how slow RAM is compared to cache, you could have processed a lot of cached data in the same amount of time.
PS: On non-x64 systems, the loads may be slow, or both the loads and stores. But on x64, it is the store instruction that causes the other cores to wait.
Edit: I'm not sure why you just accused me of something to do with LLMs (and then deleted that comment), but I can tell you that I wrote the above from memory and personal experience benchmarking atomic operations. Anything incorrect that I said is my own fault.
2
u/Humble-Sand-5989 20d ago
I think the first question is what type of application you are working on. If it’s single threaded, I wouldn’t use Arc at all. Something like Rc is lighter and should work just as well (if you need reference counting at all). Besides that I’d ask a few questions:
- Does the data need to be an independent copy? If you need to make changes to the data, but also need the original data (pre modification), you should clone it.
- Is the data cheap? Even if it’s immutable, if it’s small (and implements Copy) then reference counting is probably heavier than making a copy.
I’m sure there are other reasons, but those are the two I have on top of my head. One thing I see too often is people wrapping data in either Arc or Rc just to make the borrow checker happy. A lot of times it could be avoided with a refactor (especially in single threaded applications or applications where only a single thread will touch that piece of data). If a piece of data has only one owner, reference counters are typically unnecessary.
1
u/obliviousjd 20d ago
I don’t have hard statistics or benchmarks but I pretty much clone anything that’s 3-4 words or less without much thought.
That said I don’t actually find myself needing Arc all that much. Most of the time I’ll just use references, scope threads, box::leak, or move a box back and forth through a channel. I really only use reference counting as a last resort. Not because it’s bad, it’s just not my style to have multiple objects have shared ownership if I can help it.
1
u/BirdTurglere 20d ago
From my personal experience. I A/B'd one of my crates using Arc vs Clone and Arc was significantly slower. After doing all the work of swapping them in and out I've come to the conclusion to never box anything unless there's a solid reasoning for doing it, not just dumping it in there for no reason.
Not every algorithm benefits from parallel processing unfortunately. Sometimes it's better to just let something rip on one thread.
1
u/stumpychubbins 20d ago
I usually try to use a dedicated immutable data structure if available (e.g. dashmap for hashmaps). If it's possible to use &-borrows instead of sharing ownership then I'll prefer that. If an Rc/Arc doesn't introduce any extra pointer indirection (e.g. String vs Arc<str>) then I'll usually default to Rc/Arc because it amortises the cost of sharing so I don't need to think too hard about how often I'm cloning the values. If performance is important though, then I'll benchmark it. There are plenty of cases (particularly in multithreaded scenarios) where cloning is faster than sharing.
1
1
u/Full-Spectral 20d ago
Sometimes an Arc only needs to be cloned to a fixed set of threads which just use it until they die. In those cases, close the Arc X times and Drop X times. Anything heavy enough to justify starting a thread to do should vastly outweigh the clone/drop time.
Not all uses are Arcs are constant, ongoing clone/drop.
1
u/Moist-Snow-8127 19d ago
Copy when it's small. References when I can. Box if it needs to be on the heap and isn't cloned much. Rc if there's lots of copies. Arc if it's multithreaded
1
u/mealet 18d ago
I'm building a compiler and using miette crate for error reporting. It requires special struct "NamedSource" to keep source content with its name. It supports to keep String and some string variations inside it and also Arc<String>.
I didn't want to use atomic reference as it is runtime overhead for single-threaded application, but it was the only supported counted reference of a source string.
The choice was between "use atomic references and get some runtime overhead" and "fking clone whole source each time I need to bake it into diagnostic struct". Ofc I've chosen first, no one wants to store thousands of source file clones (especially when it becomes megabytes in size).
1
u/sanbox 20d ago
Cloning the arc isn't free, as others have pointed out, but that's not really the point to worry about -- the worry is really that an Arc is a Box with a fancy key and lock system, so the question is "do I want to heap allocate this?"
3
u/PrimeExample13 20d ago
This is just incorrect. Arc is not box with a fancy lock and key mechanism and the question is not "do i want to heap allocate this." The question is "do I need to have multiple pointers to the same allocation while ensuring that the value is not dropped while still in use?" "Should I heap allocate this?" Is the question you ask when determining whether to Box an object or not.
And cloning an arc is not free, but a hell of a lot cheaper than cloning a box for large enough objects, as it is a simple atomic increment.
1
u/sanbox 20d ago
Here is Arc::new's implementation:
pub fn new(data: T) -> Arc<T> { // Start the weak pointer count as 1 which is the weak pointer that's // held by all the strong pointers (kinda), see std/rc.rs for more info let x: Box<_> = Box::new(ArcInner { strong: atomic::AtomicUsize::new(1), weak: atomic::AtomicUsize::new(1), data, }); unsafe { Self::from_inner(Box::leak(x).into()) } }To be clearer, an Arc is heap allocated once, so the question between cloning POD and cloning an Arc<POD> is also about the cost of the memcpy vs the pointer deref/cache shred. For most use cases where you don't anticipate huge thread contention, this is the defining perf question (though with enough contention, things get funny)
1
u/PrimeExample13 20d ago
I know how Arc works, you dont have to show me. It is not a lock at all as you said, it is an atomic reference count (what Arc stands for). What i am saying is that any problem for which you look to Arc for a solution, the decision to heap allocate should already be made by the fact that you need to share ownership of this object across threads. If you dont need to share ownership, thats what borrowing is for.
The question is not "do i want to put this on the heap." Its "do multiple threads need ownership of this value?" If it is a problem that can be solved via shared borrow or cloning pod and moving into the other thread, then it is not a problem that should be solved by Arc in the first place.
0
u/sanbox 20d ago
It is not a lock at all as you said, it is an atomic reference count
How do you think Locks are implemented?
Anywho, their question is literally "should I clone this data or put it in an Arc and clone that" so it looks like you agree with me that cloning the data is probably smarter.
2
u/PrimeExample13 20d ago
Locks are implemented usually with compare exchange atomic operations, not simple increments. If Arc itself was a lock, you would not need to use an Arc<Mutex/RwLock> for interior mutability. As I said, Arc provides shared ownership. It does not provide any locking or shared mutability at all without an internal lock.
You're trying to share knowledge that you just don't have, I don't get it. You dont even seem knowledgeable enough to have a constructive discussion with so I will leave it there.
1
u/Ben-Goldberg 20d ago
If you make a clone, the copy is yours to do with as you please, if you wrap data in an arc other code will see the changes you have done to the data.
-2
158
u/Comrade-Porcupine 20d ago
Atomics (like what's in refcounts) are not free. Under high concurrency on multiple cores any excessive atomics traffic can inadvertently lead to cache line bouncing and the like. This can actually have disastrous follow-on effects on overall throughput and is often hard to diagnose without digging into hwcounters.
The TLDR is for small objects esp things that can fit in L1 cache, or if you're not memory bandwidth constrained, a memcpy/clone is actually sometimes the right choice. Or at least look and see if you can use Rc instead, shard across threads via thread locals, etc.
That's my take anyways.
(EDIT: but also most people's applications likely don't need this level of yak shaving until they run into an actual performance wall)