Most active commenters
  • woodruffw(6)
  • kouteiheika(3)

←back to thread

271 points mithcs | 12 comments | | HN request time: 0.001s | source | bottom
Show context
woodruffw ◴[] No.45953391[source]
Intentionally or not, this post demonstrates one of the things that makes safer abstractions in C less desirable: the shared pointer implementation uses a POSIX mutex, which means it’s (1) not cross platform, and (2) pays the mutex overhead even in provably single-threaded contexts. In other words, it’s not a zero-cost abstraction.

C++’s shared pointer has the same problem; Rust avoids it by having two types (Rc and Arc) that the developer can select from (and which the compiler will prevent you from using unsafely).

replies(13): >>45953466 #>>45953495 #>>45953667 #>>45954940 #>>45955297 #>>45955366 #>>45955631 #>>45955835 #>>45959088 #>>45959352 #>>45960616 #>>45962213 #>>45975677 #
kouteiheika ◴[] No.45953466[source]
> the shared pointer implementation uses a POSIX mutex [...] C++’s shared pointer has the same problem

It doesn't. C++'s shared pointers use atomics, just like Rust's Arc does. There's no good reason (unless you have some very exotic requirements, into which I won't get into here) to implement shared pointers with mutexes. The implementation in the blog post here is just suboptimal.

(But it's true that C++ doesn't have Rust's equivalent of Rc, which means that if you just need a reference counted pointer then using std::shared_ptr is not a zero cost abstraction.)

replies(2): >>45953492 #>>45953505 #
woodruffw ◴[] No.45953492[source]
To be clear, the “same problem” is that it’s not a zero-cost abstraction, not that it uses the same specific suboptimal approach as this blog post.
replies(1): >>45953527 #
1. kouteiheika ◴[] No.45953527[source]
I think that's an orthogonal issue. It's not that C++'s shared pointer is not a zero cost abstraction (it's as much a zero cost abstraction as in Rust), but that it only provides one type of a shared pointer.

But I suppose we're wasting time on useless nitpicking. So, fair enough.

replies(1): >>45953589 #
2. woodruffw ◴[] No.45953589[source]
I think they’re one and the same: C++ doesn’t have program-level thread safety by construction, so primitives like shared pointers need to be defensive by default instead of letting the user pick the right properties for their use case.

Edit: in other words C++ could provide an equivalent of Rc, but we’d see no end of people complaining when they shoot themselves in the foot with it.

(This is what “zero cost abstraction” means: it doesn’t mean no cost, just that the abstraction’s cost is no greater than the semantically equivalent version written by the user. So both Arc and shared_ptr are zero-cost in a MT setting, but only Rust has a zero-cost abstraction in a single-threaded setting.)

replies(3): >>45953732 #>>45957354 #>>45960578 #
3. kouteiheika ◴[] No.45953732[source]
I can't say I agree with this? If C++ had an Rc equivalent (or if you'd write one yourself) it would be just as zero cost as it is in Rust, both in a single-threaded setting and in a multithreaded-setting. "Zero cost abstraction" doesn't mean that it cannot be misused or that it doesn't have any cognitive overhead to use correctly, just that it matches whatever you'd write without the abstraction in place. Plenty of "zero cost" features in C++ still need to you pay attention to not accidentally blow you leg off.

Simply put, just as a `unique_ptr` (`Box`) is an entirely different abstraction than `shared_ptr` (`Arc`), an `Rc` is also an entirely different abstraction than `Arc`, and C++ simply happens to completely lack `Rc` (at least in the standard; Boost of course has one). But if it had one you could use it with exactly the same cost as in Rust, you'd just have to manually make sure to not use it across threads (which indeed is easier said than done, which is why it's not in the standard), exactly the same as if you'd manually maintain the reference count without the nice(er) abstraction. Hence "zero cost abstraction".

replies(1): >>45953853 #
4. woodruffw ◴[] No.45953853{3}[source]
Sorry, I realized I’m mixing two things in a confusing way: you’re right that C++ could easily have a standard zero-cost Rc equivalent; I’m saying that it can’t have a safe one. I think this is relevant given the weight OP gives to both performance and safety.
5. SR2Z ◴[] No.45957354[source]
Isn't the point of using atomics that there is virtually no performance penalty in single threaded contexts?

IMO "zero cost abstraction" just means "I have a slightly less vague idea of what this will compile to."

replies(1): >>45957420 #
6. SkiFire13 ◴[] No.45957420{3}[source]
No, atomics do have a performance penality compared to the equivalent single threaded code due to having to fetch/flush the impacted cache lines in the eventuality that another thread is trying to atomically read/write the same memory location at the same time.
replies(1): >>45958919 #
7. CyberDildonics ◴[] No.45958919{4}[source]
Atomics have almost no impact when reading, which is what would happen in a shared pointer the vast majority of the time.
replies(2): >>45959048 #>>45959568 #
8. woodruffw ◴[] No.45959048{5}[source]
> which is what would happen in a shared pointer the vast majority of the time.

This seems workload dependent; I would expect a lot of workloads to be write-heavy or at least mixed, since copies imply writes to the shared_ptr's control block.

9. oconnor663 ◴[] No.45959568{5}[source]
I think it's pretty rare to do a straight up atomic load of a refcount. (That would be the `use_count` method in C++ or the `strong_count` method in Rust.) More of the time you're doing either a fetch-add to copy the pointer or a fetch-sub to destroy your copy, both of which involve stores. Last I heard the fetch-add can use the "relaxed" atomic ordering, which should make it very cheap, but the fetch-sub needs to use the "release" ordering, which is where the cost comes in.
10. groundzeros2015 ◴[] No.45960578[source]
> C++ doesn’t have program-level thread safety by construction

It does. It’s called a process.

Everyone chose convenience and micro-benchmarks by choosing threads instead.

replies(1): >>45960997 #
11. woodruffw ◴[] No.45960997{3}[source]
"Thread truther" is not one of the arguments I had on the bingo card for this conversation.
replies(1): >>45961226 #
12. groundzeros2015 ◴[] No.45961226{4}[source]
I guessed as much. I’m not alone - there is a whole chapter on this topic in “The art of UNIX programming”.