pervognsen , (edited )
@pervognsen@mastodon.social avatar

It looks a bit funny but Rc<Arc<T>> seems like a reasonable choice in a lot of cases. Specifically, you have locally shared ownership of a remotely shared resource instead of directly sharing ownership of the remote resource (which comes with contention issues). Most of the time you probably wouldn't literally have Rc<Arc<T>> but Rc<LocalStruct> where LocalStruct (transitively) has an Arc<T>. But same thing really.

SonnyBonds ,
@SonnyBonds@mastodon.gamedev.place avatar

@pervognsen I've been thinking about this a bit in C++ but at that point I thought of it as some multilevel RC thing and for some reason it felt like it wouldn't work easily. But just wrapping an Arc in a Rc (or corresponding types) is a good and clear way of thinking about it!

(I did however try switching all shared_ptr for a nonatomic version and saw no apparent improvements on perf so I stopped thinking too much about it.)

pervognsen OP ,
@pervognsen@mastodon.social avatar

@SonnyBonds I think the bigger problem with std::shared_ptr is the extra indirection. In Rust, Rc<T> and Arc<T> puts the refcount next to the allocated value, there's no indirection through a control block. If you want an extra indirection (which can occasionally be useful) you'd explicitly use Rc<Box<T>> where Box<T> is the equivalent of std::unique_ptr<T>.

SonnyBonds ,
@SonnyBonds@mastodon.gamedev.place avatar

@pervognsen Well unless I'm misunderstanding, it only needs one indirection for accessing the data and one for accessing the refcount since it stores both pointers (which may be a problem in itself by all means). But yes when doing both it needs to dereference two independent pointers.

There are of course other implementations than shared_ptr though, and std doesn't even have a non-atomic one. Was just thinking conceptually about the nesting.

pervognsen OP ,
@pervognsen@mastodon.social avatar

@SonnyBonds Ah, I must have misremembered. I just remembered there was a separately allocated control block. I haven't actually ever used std::shared_ptr in real shipping software. In fact, I don't think I've ever worked on a C++ codebase where reference counting was the main memory management strategy. Usually it was used for specialized data types, like assets, and there you usually needed some special features anyway.

SonnyBonds ,
@SonnyBonds@mastodon.gamedev.place avatar

@pervognsen We (Kilohearts) actually use it a lot for UI parts. It's a "retained" UI (not IM) and we chose to use shared_ptr for all UI structure, in the somewhat hated "use shared_ptr for everything" way. I know it's pretty frowned upon, but it's just pretty much a predictable GC, and I think it actually works really well. That part very rarely causes problems.

artificialmind ,
@artificialmind@fosstodon.org avatar

@pervognsen @SonnyBonds they are two pointers but if you use std::make_shared (which is the idiomatic way nowadays), then it only does a single allocation where control and data block are adjacent.

pervognsen OP ,
@pervognsen@mastodon.social avatar

@artificialmind @SonnyBonds Oh, nice. It looks like that was already available in C++11 judging from cppreference?

artificialmind ,
@artificialmind@fosstodon.org avatar

@pervognsen @SonnyBonds Yep and it was (at least in my perception) always advertised and taught as "the modern/idiomatic way" when using smart pointers. std::make_shared also has some exception-safety benefits where a throwing ctor doesn't lead to leaking memory.

The only real "downside" with shared allocation is that weak pointers can keep the data allocation alive even if the data itself is not accessible anymore. But I haven't encountered that issue in real code yet.

pervognsen OP ,
@pervognsen@mastodon.social avatar

@artificialmind @SonnyBonds Yeah, that downside with weak references keeping the allocation alive is the thing I had in mind when I said that in Rust it's still useful occasionally to use Rc<Box<T>>. With plain Rc<T>, it will in-place drop/destruct the value when the strong count goes to zero but it can't drop the allocation itself until the weak count goes to zero as well.

SonnyBonds ,
@SonnyBonds@mastodon.gamedev.place avatar

@artificialmind @pervognsen We (ab)use weak_ptrs quite a lot for some event listener management and stuff so we don''t use make_shared for a lot of stuff. (We have a macro for Reasons for our UI components and it doesn't do make_shared.) It probably doesn't make a difference as the structures are neither huge nor allocated/deallocated a lot, but feels conceptually right.

For some other things that are more blobs of data and stuff we do use make_shared though. More of a vibes based policy. :)

pervognsen OP ,
@pervognsen@mastodon.social avatar

@SonnyBonds @artificialmind FWIW, the last time I wrote an old fashioned RMUI with explicit create/destroy lifecycles I used arenas with weak handles (array per widget type, weak handles are generation + index), basically the same thing many games do for game objects. It worked really well. I think using shared pointers for this kind of thing is a bit weird, personally, because the "logical" lifecycle of widgets is completely separate from the "physical" lifecycle that shared_ptr manages.

pervognsen OP ,
@pervognsen@mastodon.social avatar

@SonnyBonds @artificialmind Which doesn't mean it's "wrong" to use them, I just prefer the arena + weak handle approach myself for something like this.

SonnyBonds ,
@SonnyBonds@mastodon.gamedev.place avatar

@pervognsen @artificialmind Yes I totally see what you mean. Conceptually it's not really a 1:1 match, especially since the resources aren't really shared but has a pretty strict ownership hierarchy. The thing I like about it is that the refcount gives you lifetime extension. As long as you hold a ref you know it's valid, and can avoid rugpulling stuff like a component removing itself while it's processing an event etc etc without deferred freeing.

pervognsen OP ,
@pervognsen@mastodon.social avatar

@SonnyBonds @artificialmind Yeah, I sometimes also use arenas with both strong and weak handles for exactly that reason. One of the things I like about "localizing" this kind of thing to a specific arena is that you can make stronger assumptions, e.g. instead of relying on temporarily upgrading weak pointers to strong pointers in order to lock them in memory for the extent of a "transaction", you could do something more specialized.

pervognsen OP ,
@pervognsen@mastodon.social avatar

@SonnyBonds @artificialmind For example, maybe you just introduce the concept of a grace period/epoch. Once per frame at the top of the main loop you do all the clean-up. At that point nothing else can be on the stack that has stuff "locked". That often works really well. Even with multi-threading added to the mix (probably not a concern for RMUI but mentioning it just in case) you have epoch-based reclamation (EBR) and quiescent state-based reclamation (QSBR) which is similar but more flexible.

SonnyBonds ,
@SonnyBonds@mastodon.gamedev.place avatar

@pervognsen @artificialmind Yeah there probably are better ways. I've messed a bit with arenas lately just prototyping things but haven't quite internalized how to work with it in a way that I really like.

pervognsen OP ,
@pervognsen@mastodon.social avatar

@SonnyBonds @artificialmind The "don't physically recycle anything until the end of the main loop" thing is the classic game solution as well. Works just as well for a "pure UI" without a traditional concept of frames, too, since you have an equivalent of the main loop in the form of the event loop. If you know autorelease pools in Objective C, it's kind of similar.

SonnyBonds ,
@SonnyBonds@mastodon.gamedev.place avatar

@pervognsen @artificialmind Yes absolutely. I think a complicating (but not disqualifying) factor in our case is that we're making plugins and don't really have control over the "main loop". There's rather multiple entry points from both host and system. It's of course still possible to do it, autorelease-style or otherwise, but it makes it a little bit more annoying.

pervognsen OP ,
@pervognsen@mastodon.social avatar

@SonnyBonds P.S. I finally got the Kilohearts pun. :)

SonnyBonds ,
@SonnyBonds@mastodon.gamedev.place avatar

@pervognsen Haha it helps to know it's about audio. :D

pervognsen OP , (edited )
@pervognsen@mastodon.social avatar

Chaining Rc/Arc seems similar to the pattern of distributed counting in parallel programming where you have a tree of counters to reduce contention [1]. For the same reason Arc<Arc<T>> could also make sense if you need to reduce contention.

[1] For example, see the chapter in https://arxiv.org/abs/1701.00854

  • All
  • Subscribed
  • Moderated
  • Favorites
  • random
  • tech
  • kbinEarth
  • testing
  • interstellar
  • wanderlust
  • All magazines