# 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.

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.