Pattern to use when avoiding `Arc` or similar smart pointers

I have struct Metadata which is quite big in size. Ideally I want to have only single instance of it in memory. I have another struct Polynomials that requires
access to Metadata for performing operations in its methods. I understand that I can store smart pointer to Metadata inside Polynomial using Arc. But I have decided
against it, since applications that library targets are multithreaded and Polynomials are created and dropped many times. I also don't want to force overhead of
Arc to library users.

So I could think of following three options to implement what I want. First is quite straightforward. I simply pass Metadata as reference to each Polynomial operation. For example,

struct Metadata {}

struct Polynomial {
    data: Vec<u64>,
}

impl Polynomial {
    fn add(&self, b: &Polynomial, metadata: &Metadata) -> Polynomial {
        // Checks metadata is correct for polynomials before doing the operation, only in debug mode
        todo!()
    }
}

Problem with this apporach is that I have to always rememeber to pass correct metadata with each function call. An alternate to this is creating a Evaluator struct like following:

struct Evaluator<'a> {
    metadata: &'a Metadata,
}

impl Evaluator<'_> {
    fn add(&self, a: &Polynomial, b: &Polynomial, metadata: &Metadata) -> Polynomial {
        // Checks metadata is correct for polynomials before doing the operation, only in debug mode
        todo!()
    }
}

The benefit of using Evaluator is that I will not have to rememeber to pass correct metadata with each function call, instead correct polynomials. This seems to be a better option
if I have to perform a bunch of polynomial operations specific to a given metadata within a given scope. So far this seems to be the case.

Another approach can be creating PolynomialWithMetadataRef struct like

struct PolynomialWithMetadataRef<'a> {
    data: &'a [u64],
    metadata: &'a Metadata,
}

impl PolynomialWithMetadataRef<'_> {
    fn add(&self, a: &PolynomialWithMetadataRef, metadata: &Metadata) -> Polynomial {
        todo!()
    }
}

The nice thing about 3rd approach is that it avoids having to check that metadata is correct for polynomials (only in debug mode) at every method call. Instead we can perform the check when
creating PolynomialWithMetadataRef from Polynomial, like


impl Polynomial {
    pub fn view<'a>(&'a self, metadata: &'a Metadata) -> PolynomialWithMetadataRef {
        // Checks metadata is correct for polynomials, only in debug mode
        PolynomialWithMetadataRef {
            data: &self.data,
            metadata,
        }
    }
}

With this I can also implement std::ops trait for PolynomialWithMetadataRef. But again, I will have to rememeber to pass correct metadata at the time of calling Polynomial::view.

I plan to stick to having asserts in debug mode only. So as far as I understand, all three will have equivalent performance at runtime. So the question is of only what's
easier for me to handle as the codebase grows.

I am curious whether I have missed anything? Open to suggestions to other patterns as well :slight_smile:

What's the overhead of Arc you want to avoid? Cloning/dropping Arc is trivial compared to constructing/dropping Vec<u64> which the Polynomial in the example already holds. It's just a single atomic operation.

6 Likes

There is overhead by making it atomic. That's why the documentation for Arc recommends Rc if you don't share references between threads.
In Rust you just don't have the possibility to have non-atomic reference counting in a multithreaded context, which makes people who like buggy software unhappy :wink:

I'm not saying Arc has no overhead. It's that the Vec also has overhead, actually dozen times more than Arc, so if you can live with the Vec there's no need to avoid another Arc since it's not significant.

Edit: fixed missing "no", ofc Arc has its overhead.

2 Likes

I guess you wanted to say "I'm not saying Arc has no overhead" and I totally agree with you that it's not significant.
Even if it was significant overhead, it would be a necessity and couldn't be avoided.

I expect the applications using the library to run many cores while still holding single instance of Metadata and I am under impression that Cloning/dropping Arc can add to the overhead. I could be mistaken and on modern processors it does not impact the performance. In which case Arc sounds good.

On the other hand, I am confused that if I can find a way to share Metadata without Arc, for example using Evaluator, then is there any specific reason to use Arc? Probably so that code is mangeable? Or would there be other reasons as well?

In what situations does it become a necessity ?

Being multi-threaded. To be precise: using a reference in multiple threads (you can have a multi-threaded application, but things used only in one thread or are moved between threads don't require thread-safety).
As you want to use a reference to your metadata from different threads, you have to have the reference-counting done in a thread-safe way. That's the purpose of Arc.

Performance is always relative. An apple is light and a melon is heavy. But if you're riding a truck, replacing an apple on your cargo with a melon doesn't matter at all due to the weight of the truck itself.

It can matter if your code is full of cloning Arcs. But in this case you're only cloning it on the creation of the Polynomial which also constructs Vec, and only drop it when the Vec also is dropped. The Vec is the truck here, an Arc will not be the bottleneck in this pattern.

In other words, is there any specific reason to use something like Evaluator if the performance is on par? It surely increases complexity a lot. But what's the benefit that justifies it?

8 Likes

I can't resist to quote one of the great old timers of IT:
"Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are
considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil."

6 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.