Why is size_of<Arc<str>> == 16?

I read a puzzling blog post about reducing the number of Deref when using strings. It showed that size_of::<Arc::<str>> == 16.

The explanation from the blog post is not satisfactory to me. It claims that the size is 16 because str is a fat pointer. But Arc<T> contains a pointer to ArcInner<T> not to T. So it should not make a difference if the T is String or str.

An Arc<T> contains a pointer to ArcInner<T> which consists of two reference counters (for strong and weak references) and T. ArcInner<String> has size 40 (8 + 8 + 32) and ArcInner<str> has size 32 (8 + 8 + 16) as expected. (String contains a usize for capacity and str does not).

So why is the Arc<str> size 16? The extra 8 bytes are not needed. Is this a bug in size_of? Or is there some optimization going on where either the pointer or the length of the str is present in each Arc<str> and Arc<[T]>?

The size of Arc for slices was previously discussed here:

I'm not sure where you got that from, but I don't think it's true - ArcInner<T> stores the T directly inline, so if the T is unsized the ArcInner<T> will be as well: 8 + 8 + !Sized = !Sized. The str type itself is not 16 bytes, it is only pointers to the str type that are 16 bytes, and ArcInner<str> doesn't store any pointers to it.

2 Likes

Right, it should be &str. I tried with a local version of ArcInner copied from alloc/src/sync.rs.

#[repr(C)]
struct ArcInner<T: ?Sized> {
    strong: AtomicUsize,
    weak: AtomicUsize,
    data: T,
}
fn test() {
    println!("sizeof ArcInner<&str> {}", size_of::<ArcInner<&str>>());
    println!("sizeof NonNull<ArcInner<str>> {}", size_of::<NonNull<ArcInner<str>>());
}

What's in the 16 bytes in Arc<str>? At least 8 bytes should be a pointer to the InnerArc<str>.

An Arc<&str>, which contains an ArcInner<&str>, is indeed 8 bytes. But an Arc<str>, which contains the unsized ArcInner<str>, is 16 bytes, because the str is stored inline inside the Arc's allocation instead of through a wide pointer. The 16 bytes in NonNull<ArcInner<str>> contains the address of the data and the length of the string, just how &str or NonNull<str> would.

1 Like

Then where are the reference counters?

ArcInner<str> contains the reference counts and the string contents, laid out like this:

+--------+--------+--------- - - -
| strong | weak   | string data...
+--------+--------+--------- - - - 

Arc<str> contains a pointer to the ArcInner<str> and the length of the string, laid out like this:

+--------+--------+
| ptr    | length |
+--------+--------+

That is a fascinating optimization. I had wondered how to make Arc<String> avoid the double dereference and so the claim from the blog post holds up. And the same is true for Arc<Vec<T>> versus Arc<[T]>.

Clippy never told me. :wink:

Clippy's rc_buffer lint warns about this, but it's in the “restriction” group which is not enabled by default, because there are some use cases where Arc<String> is better despite the extra layer of indirection.

Actually ran into this issue as well, and it's definitely a pain. Fundamentally, there is no way for rust to work with dynamically sized types other than through fat pointers, which will always infect the first level of indirection.

For my purpose, the solution I went with is to actually use double indirection, so Box<Box<[T]>>, since size was absolutely essential, and this would be wrapped in an option and mostly None anyway. I also thought about using a global arena of Box<[T]>, since 32 bit indices would be smaller than the 64 bit pointer and reduce the runtime cost of the outer lookup, but keeping track of drops and what not actually became a real pain.

It would be extremely nice if rust could expose a C-style DST that avoids fat pointers. Like, a "special" structure that can only be implemented in the standard library (similar to UnsafeCell), that stores a slice as (length, [T]) that knows how to check for safe access and such, similar to a slice, except sizeof(Box) == sizeof(Box<()>). You couldn't convert to or from Vec directly, since the heap allocation would also need to store the length, but it'd be really nice to have this.

Sorry for any typos; on my phone.

There are some third-party crates like thin_str, thin-vec, and slice-dst that implement patterns like that.

2 Likes

I'd like to point out the following quote in the footnotes of the article that was linked:

Technically, the From<String> implementation just dereferences the string and copies the underlying str into the Arc allocation.

In other words, even if you own the original string buffer, copying is still necessary to create the Arc, and this is because of those inlined refcounts. This can potentially make Arc<[T]>/Rc<[T]> an unwise choice for particularly large vectors. (e.g. within an order of magnitude of the total available memory, where the allocation of the ArcInner could potentially fail)

1 Like

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.