Live data from Hacker News

A safe, non-owning C++ pointer class

techblog.rosemanlabs.com

41–50 of 50 posts

Re: A safe, non-owning C++ pointer class

#41
post #27
post #2

> Note that our use case is in a single-threaded context. Hence, the word safe should not be interpreted as ‘thread-safe.’ Single-threadedness greatly simplifies the design; we need not reason about race conditions such as one where an object is simultaneously moved and accessed on different threads. Extending the design to a thread-safe one is left as an exercise to the reader. Why intentionally design a worse alter…

We purposefully didn't use shared_ptr and hence weak_ptr. With these, it is all too easy to construct the "bad" version which has the stub reference count and pointer stored far away in memory from the object itself requiring a double dereference to access the object which is bad for cache performance. Instead we derived off a shareable class that has the reference count to make sure it is close in memory. We were ha…

With make_shared it's guaranteed to be a single allocation these days so you shouldn't necessarily have cache locality problems. I do think there are benefits to things being intrusively recounted as you save 8 bytes per object. And if you give up a weak count you can save even more.

Re: A safe, non-owning C++ pointer class

#42

This just seems intentionally bad to show where Rust would be better. This is yet another example of what I call "corner-case" instruction, which I define as, "I am going to take an obviously terrible corner-case that shows what an awful developer can do that will break a program, then demonstrate my brilliance by introducing my (highly-biased) opinionated point I wanted to make..." In this particular case, it was su…

You could implement the same smart pointer library in rust and it would be fine. Rust doesn't magically solve the problems around defined destruction ordering when using ref counted pointers. I try very hard to model my usage of Rc or Arc to be very similar to what this article is trying to showcase for basically the same reasons I imagine they do. I'm actually inspired to write a crate with these semantics to make i…

Sure enough I found it: https://crates.io/crates/refbox

Re: A safe, non-owning C++ pointer class

#43
post #2

> Note that our use case is in a single-threaded context. Hence, the word safe should not be interpreted as ‘thread-safe.’ Single-threadedness greatly simplifies the design; we need not reason about race conditions such as one where an object is simultaneously moved and accessed on different threads. Extending the design to a thread-safe one is left as an exercise to the reader. Why intentionally design a worse alter…

The atomics in std::weak_ptr are >20x more expensive even with 0 contention.

Re: A safe, non-owning C++ pointer class

#44

Earlier quoted context omitted.

I never heard of this and went to check in the source and it really does exist: https://codebrowser.dev/llvm/include/c++/11/ext/concurrence....

The code you linked is a compile-time configuration option, which doesn't quite match "infer" IMO. I think GP is thinking of the way that libstdc++ basically relies on the linker to tell it whether libpthread is linked in and skips atomic operations if it isn't [0]. [0]: https://snf.github.io/2019/02/13/shared-ptr-optimization/

It's a compile-time flag which is defined when libpthread is linked into the binary.

Re: A safe, non-owning C++ pointer class

#45

Earlier quoted context omitted.

The code you linked is a compile-time configuration option, which doesn't quite match "infer" IMO. I think GP is thinking of the way that libstdc++ basically relies on the linker to tell it whether libpthread is linked in and skips atomic operations if it isn't [0]. [0]: https://snf.github.io/2019/02/13/shared-ptr-optimization/

It's a compile-time flag which is defined when libpthread is linked into the binary.

Sure, but I think that's independent of what eMSF was describing. From libgcc/gthr.h:

    /* If this file is compiled with threads support, it must
           #define __GTHREADS 1
       to indicate that threads support is present.  Also it has define
       function
         int __gthread_active_p ()
       that returns 1 if thread system is active, 0 if not.
I think the mechanism eMSF was describing (and the mechanism in the blogpost I linked) corresponds to __gthread_active_p().

I think the distinction between the two should be visible in some cases - for example, what happens for shared libraries that use std::shared_ptr and don't link libpthread, but are later used with a binary that does link libpthread?

Re: A safe, non-owning C++ pointer class

#46

Earlier quoted context omitted.

It's a compile-time flag which is defined when libpthread is linked into the binary.

Sure, but I think that's independent of what eMSF was describing. From libgcc/gthr.h: /* If this file is compiled with threads support, it must #define __GTHREADS 1 to indicate that threads support is present. Also it has define function int __gthread_active_p () that returns 1 if thread system is active, 0 if not. I think the mechanism eMSF was describing (and the mechanism in the blogpost I linked) corresponds to _…

Hm, not sure. I can see that shared_ptr::_M_release [0] is implemented in terms of __exchange_and_add_dispatch [1] and which is implemented in terms of __is_single_threaded [2]. __is_single_threaded will use __gthread_active_p iff __GTHREADS is not defined and header not included.

Implementation of __gthread_active_p is indeed a runtime check [3] which AFAICS applies only to single-threaded programs. Perhaps the shared-library use-case also fits here?

Strange optimization IMHO so I wonder what was the motivation behind it. The cost function being optimized in this case is depending on WORD being atomic [4] without actually using the atomics [5].

[0] https://codebrowser.dev/llvm/include/c++/11/bits/shared_ptr_...

[1] https://codebrowser.dev/llvm/include/c++/11/ext/atomicity.h....

[2] https://codebrowser.dev/llvm/include/c++/11/ext/atomicity.h....

[3] https://codebrowser.dev/kde/include/x86_64-linux-gnu/c++/11/...

[4] https://codebrowser.dev/llvm/include/c++/11/ext/atomicity.h....

[5] https://codebrowser.dev/llvm/include/c++/11/ext/atomicity.h....

Re: A safe, non-owning C++ pointer class

#47

Earlier quoted context omitted.

Sure, but I think that's independent of what eMSF was describing. From libgcc/gthr.h: /* If this file is compiled with threads support, it must #define __GTHREADS 1 to indicate that threads support is present. Also it has define function int __gthread_active_p () that returns 1 if thread system is active, 0 if not. I think the mechanism eMSF was describing (and the mechanism in the blogpost I linked) corresponds to _…

Hm, not sure. I can see that shared_ptr::_M_release [0] is implemented in terms of __exchange_and_add_dispatch [1] and which is implemented in terms of __is_single_threaded [2]. __is_single_threaded will use __gthread_active_p iff __GTHREADS is not defined and header not included. Implementation of __gthread_active_p is indeed a runtime check [3] which AFAICS applies only to single-threaded programs. Perhaps the shar…

> Implementation of __gthread_active_p is indeed a runtime check [3] which AFAICS applies only to single-threaded programs. Perhaps the shared-library use-case also fits here?

The line you linked is for some FreeBSD/Solaris versions which appear to have some quirks with the way pthreads functions are exposed in their libc. I think the "normal" implementation of __gthread_active_p is on line 248 [0], and that is a pretty straightforwards check against a weak symbol.

> Strange optimization IMHO so I wonder what was the motivation behind it.

I believe the motivation is to avoid needing to pay the cost of atomics when there is no parallelism going on.

> The cost function being optimized in this case is depending on WORD being atomic [4] without actually using the atomics [5].

Not entirely sure what you're getting at here? The former is used for single-threaded programs so there's ostensibly no need for atomics, whereas the latter is used for non-single-threaded programs.

[0]: https://codebrowser.dev/kde/include/x86_64-linux-gnu/c++/11/...

Re: A safe, non-owning C++ pointer class

#48

Earlier quoted context omitted.

Hm, not sure. I can see that shared_ptr::_M_release [0] is implemented in terms of __exchange_and_add_dispatch [1] and which is implemented in terms of __is_single_threaded [2]. __is_single_threaded will use __gthread_active_p iff __GTHREADS is not defined and header not included. Implementation of __gthread_active_p is indeed a runtime check [3] which AFAICS applies only to single-threaded programs. Perhaps the shar…

> Implementation of __gthread_active_p is indeed a runtime check [3] which AFAICS applies only to single-threaded programs. Perhaps the shared-library use-case also fits here? The line you linked is for some FreeBSD/Solaris versions which appear to have some quirks with the way pthreads functions are exposed in their libc. I think the "normal" implementation of __gthread_active_p is on line 248 [0], and that is a pre…

> Not entirely sure what you're getting at here?

> I believe the motivation is to avoid needing to pay the cost of atomics when there is no parallelism going on.

Obviously yes. What I am wondering is what benefit does it bring in practice. Single-threaded program with shared-ptr's using atomics vs shared-ptr's using WORDs seem like a non-problem to me - e.g. I doubt it has a measurable performance impact. Atomics are slowing down the program only when it comes to contention, and single-threaded programs can't have them.

Re: A safe, non-owning C++ pointer class

#49

Earlier quoted context omitted.

> Implementation of __gthread_active_p is indeed a runtime check [3] which AFAICS applies only to single-threaded programs. Perhaps the shared-library use-case also fits here? The line you linked is for some FreeBSD/Solaris versions which appear to have some quirks with the way pthreads functions are exposed in their libc. I think the "normal" implementation of __gthread_active_p is on line 248 [0], and that is a pre…

> Not entirely sure what you're getting at here? > I believe the motivation is to avoid needing to pay the cost of atomics when there is no parallelism going on. Obviously yes. What I am wondering is what benefit does it bring in practice. Single-threaded program with shared-ptr's using atomics vs shared-ptr's using WORDs seem like a non-problem to me - e.g. I doubt it has a measurable performance impact. Atomics are…

> What I am wondering is what benefit does it bring in practice. Single-threaded program with shared-ptr's using atomics vs shared-ptr's using WORDs seem like a non-problem to me - e.g. I doubt it has a measurable performance impact.

I mean, the blog post basically starts with an example where the performance impact is noticeable:

> I found that my Rust port of an immutable RB tree insertion was significantly slower than the C++ one.

And:

> I just referenced pthread_create in the program and the reference count became atomic again.

> Although uninteresting to the topic of the blog post, after the modifications, both programs performed very similarly in the benchmarks.

So in principle an insert-heavy workload for that data structure could see a noticeable performance impact.

> Atomics are slowing down the program only when it comes to contention, and single-threaded programs can't have them.

Not entirely sure I'd agree? My impression is that while uncontended atomics are not too expensive they aren't exactly free compared to the corresponding non-atomic instruction. For example, Agner Fog's instruction tables [0] states:

> Instructions with a LOCK prefix have a long latency that depends on cache organization and possibly RAM speed. If there are multiple processors or cores or direct memory access (DMA) devices, then all locked instructions will lock a cache line for exclusive access, which may involve RAM access. A LOCK prefix typically costs more than a hundred clock cycles, even on single-processor systems. This also applies to the XCHG instruction with a memory operand.

And there's this blog post [1], which compares the performance of various concurrency mechanisms/implementations including uncontended atomics and "plain" code and shows that uncontended atomics are still slower than non-atomic operations (~3.5x if I'm reading the raw data table correctly).

So if the atomic instruction is in a hot loop then I think it's quite plausible that it'll be noticeable.

[0]: https://www.agner.org/optimize/instruction_tables.pdf

[1]: https://travisdowns.github.io/blog/2020/07/06/concurrency-co...

Re: A safe, non-owning C++ pointer class

#50

Earlier quoted context omitted.

> Not entirely sure what you're getting at here? > I believe the motivation is to avoid needing to pay the cost of atomics when there is no parallelism going on. Obviously yes. What I am wondering is what benefit does it bring in practice. Single-threaded program with shared-ptr's using atomics vs shared-ptr's using WORDs seem like a non-problem to me - e.g. I doubt it has a measurable performance impact. Atomics are…

> What I am wondering is what benefit does it bring in practice. Single-threaded program with shared-ptr's using atomics vs shared-ptr's using WORDs seem like a non-problem to me - e.g. I doubt it has a measurable performance impact. I mean, the blog post basically starts with an example where the performance impact is noticeable: > I found that my Rust port of an immutable RB tree insertion was significantly slower…

Thanks, I'll revisit your comment. Some interesting things you shared.
Post reply on HN