Create &'static Box<T> at compile time

At present, if you want to statically initialize parameters in code, then you often need Cow<'static,[T]> fields, so that slice types could be created both at runtime via Cow::Owned(vec) and at compile time via Cow::Borrowed(&[..]).

This Cow trick enables shallow clones nicely, but maybe incurs a variant check. I'd hope rustc optimizes away that variant check!

You'd never use &'static Box<[T]> directly, since it incurs an indirection, without any benefits, but..

If you never clone the data, then you might've some big parameters struct containing many slices, so why not use Box<[T]> and create them at compile time?

pub struct Parameters {
    foo: Box<[u64]>,
}

static PARAMETERS: &'static Parameters = &Parameters {
    foo: unsafe { box_from_static_slice(&[1,2,3,4,5,6]) },
};

pub const unsafe fn box_from_static_slice<T: 'static>(x: &'static [T]) -> Box<[T]> {
    unsafe { core::mem::transmute(x) }
}

In this case, rustc complains it "encountered mutable reference or box pointing to read-only memory" under [E0080], with an acknoledgement that "this check might be overzealous".

Should this be allowed? Or should we simply say that Cow<'static,[T]> is idomatic here, and that rustc must optimize away the variant check?

In fact, there maybe an optimization conflict here: Cow<'static,[T]> needs three usize and the variant, while Box<[T]> needs only two usize, so if rustc optimizes agressively for space then it'll treat the NonNull pointer as the variant flag, which then prevents optimizing away the variant check.

Ergo, should we use some ToOwnedBox trait with <[T] as ToOwned>::Owned = Box<[T]> and a corresponding CowBox<'a,T> type, which definitely saves one usize? If so, would rustc optimize away the variant check here?

I have idly wanted &'static Arc<T> a few times for an API that takes an Arc but also would be nice to have some static options as well. This is even more of a perf hit because if you don’t change anything else you still pay for all the increments and decrements; the only thing you really save is the allocation. But sometimes that would be nice!

There are various variant-based solutions out there for this, but honestly just being able to allocate an Arc statically would be sufficient, given that there would be a permanent root (the global). Box seems even simpler to get working in that sense: if you use Box::new in a static context, you get a pointer to static data that lives over somewhere else. Being able to reinterpret a static pointer is just one more step.

…That said, there are two things I can see getting in the way. The first is Allocator metadata—if we ever have Allocator methods that allow you to ask questions about live allocations, GlobalAlloc will have to at least handle being asked about static Boxes or Arcs. The second is putting the static allocation inside an UnsafeCell of some kind—oops, now the root isn’t permanent. Both of these can be dealt with, but they do make things a little more complicated than just “define some const behaviors for these”.

I guess I digressed from the original idea a bit, which was saying that a & &'static T (sic) could be argued to already be a valid &Box<T>.

It does: `Cow<[T]>` layout forces unnecessary branching · Issue #117763 · rust-lang/rust · GitHub

2 Likes

All cool, so CowBox saves only memory, vs Cow. This helps when you access some static data lots, like the tables used in FFTs or binary field multiplications. It's only the variant check that's worring here, not the extra memory.

I should've said &'static Params { .. Box<T> .. } for the title probably..

An &'static Arc<T> would only help if you really never cloned, becasue any Arc::clone would increment the counters and trigger segfaults, no? It's unsafe.

I suspect std::sync could be forked to add pseudo-variants everywhere that bypass the atomics, while being 100% layout compatible with std::sync, so their non-static variants could be cast into their regular forms for free: Arc could use the same NonNull<ArcInner> like Arc does, but strong = usize::MAX means ignore the atomics entirely, and you check the strong niche by doing a non-atomic read first. Mutex etc could similar have static variants, created by tweaking their poisoning flag, again checking the variant using a non-atomic read first. You maybe have something more complex going on though, ala allocator features.

No, you put the allocation in static RW memory and it’s fine (like any static in Rust with interior mutability). Not quite as good as static RO memory, but still an improvement over a new allocation that gets copied from static RO memory. (Which is not to say an “immortal” representation can’t be considered, but it’s not a prerequisite.)

1 Like

What about having static mut? This compiles

One problem here is that in general it is hard to say when this would be sound. For instance, &'static Mutex<Box<T>> is unsound since I could, at runtime, mem::swap that box with another one and then drop it -- which is UB since the box wasn't actually allocated with the runtime allocator.

The error you encounter exits because we are not sure that things would be sound without that error. That said, it does seem very plausible that you need unsafe to get yourself into that situation, so we could then blame any soundness issues on you using unsafe incorrectly.

The other reason we have this check is that I don't fully trust the static const checks reliably rejecting certain patterns in const contexts. So I wanted code like this to be rejected by a 2nd line of defense:

static OH_NO: &mut i32 = &mut 42;

That is achieved by the error you ran into. We could make the error smarter, suppressing it behind shared references, but interior mutability makes that non-trivial.

2 Likes

What about a constructor for making &'a Box<T> from &'a &'a T? Miri suggests that doing a raw transmute is sound, so the standard library should be able to make a const constructor for it. And since you only ever get a shared reference to the box, you won't call the destructor so you don't need to worry about allocating.

As for statically-constructed Arcs, this seems to me to be a benefit of handling overflow the way the linux kernel's Arcs do, by saturating at the limit and leaking the value instead of aborting. I have a library with an Arc-like thing I've been working on, and I can statically construct it by starting with the count already saturated, so it will never try to free it. And I think this change to Arc would be allowed, since I don't see anything in the docs promising this behavior.

These don't solve the issue of potentially including an allocator API for querying info about an allocation, but if that's a concern, we could make the constructors return a Box/Arc referencing some stdlib-private allocator that users can't get at so they can't try to call those methods.

I have a silly-sounding question.

Why opt for &’static Box<T> when you can just leak a Box<T> and obtain a &’static T?

What is the advantage of putting the former (and its reference-counting siblings) behind a &’static shared borrow vis a vis the latter?

EDIT:

Upon further reflection, I see only downsides to the former, as you get additional pointer chasing (and accompanying cache misses) due to the double indirection.

So it’s not a rhetorical question. I'm assuming that despite the downsides there is a valid use case for putting a box/rc/arc value behind an additional &’static borrow.

2 Likes

I think this is for cases where the static in question is typically being used as a Box<T>/Arc<T>, and thus some trickery would be necessary to use it in its native fashion. I’d love to see some concrete examples of this, but I am not doubtful they exist.

Side note: I don’t think “pointer chasing” concerns really hold water on global statics. My guess is they are perfectly predicted, and in any case global statics aren’t really something we have n of.

That was never the question. I literally wrote:

The real question was: If we have a complex type with internal allocation, then how should we best create some compile-time deeply 'static flavor? So &'static Parameters where Parameters contains a Box<[T]> in read only memory?

You cannot simply leak that foo: Box<[T]> because you cannot even create that foo during regular production runtime. If foo were FFT parameters, then you might blow your whole FFT optimization recreating them every time your consumer loads your code. Yet, you do want roughly the same algebraic operations available when creating foo. If foo were AI weghts, then you've fresh local layers, but the deeper layers cost serious money. If foo comes from some cryptographic ceremony, then foo could only be created during a meeting by specific trusted people, but you might've dynamic flavors being created all the time.

Above, the answer has two parts:

  1. Cow<'static,[T]> incurs no variant check, making it typically free when used immutably in Parameters. It's not free when used mutably of course. This answer works for types you initialize in code during development, but want statically initialization in production. Afaik this covers every case I'll hit.

  2. Interior mutability could mess up &'static Parameters, like if Parameters contains Mutex<T>. Yet, Box<T>: Freeze whenever T: Freeze, so maybe E0080 could be silenced using Freeze?

Box<T>[1] is always Freeze. Freeze does not mean an absence of interior mutability generally, only shallowly -- before indirection. And Box is all about indirection.


  1. with the default allocator ↩︎

4 Likes