storing intrusive_ptr in atomic?!
Hello, do you confirm that it is valid to store a intrusive_ptr inside an atomic?! struct X { std::size_t use_count; X() : use_count( 0) {} friend inline void intrusive_ptr_add_ref( X * p) BOOST_NOEXCEPT { ++p->use_count; } friend inline void intrusive_ptr_release( X * p) { if ( 0 == --p->use_count) delete p; } }; X::ptr_t x1( new X() ); X::ptr_t x2( new X() ); boost::atomic< X::ptr_t > av1( x1); BOOST_ASSERT( av1.compare_exchange_strong( x1, x2) ); BOOST_ASSERT( av1.load() != x1); BOOST_ASSERT( av1.load() == x2); A type T used in generic template atomic<T> is required to be trivial copy-able, e.g. I think it is trivial copy-able (std::memcpy) only the use_count will not incremented. The memory layout of intrusive_ptr is a raw-pointer so trivial copy-able should be correct. What do you think? Oliver
On Friday 25 October 2013 09:35:55 Oliver Kowalke wrote:
Hello,
do you confirm that it is valid to store a intrusive_ptr inside an atomic?!
struct X { std::size_t use_count; X() : use_count( 0) {} friend inline void intrusive_ptr_add_ref( X * p) BOOST_NOEXCEPT { ++p->use_count; } friend inline void intrusive_ptr_release( X * p) { if ( 0 == --p->use_count) delete p; } };
X::ptr_t x1( new X() );
X::ptr_t x2( new X() );
boost::atomic< X::ptr_t > av1( x1);
BOOST_ASSERT( av1.compare_exchange_strong( x1, x2) ); BOOST_ASSERT( av1.load() != x1); BOOST_ASSERT( av1.load() == x2);
A type T used in generic template atomic<T> is required to be trivial copy-able, e.g. I think it is trivial copy-able (std::memcpy) only the use_count will not incremented. The memory layout of intrusive_ptr is a raw-pointer so trivial copy-able should be correct.
What do you think?
intrusive_ptr is not trivial-copyable, precisely because it has to operate on the counter. Using memcpy for copying it is not correct. For this reason it is not compatible with atomic<>. You have to use raw pointers with atomic<>.
2013/10/25 Andrey Semashev
intrusive_ptr is not trivial-copyable, precisely because it has to operate on the counter. Using memcpy for copying it is not correct.
For this reason it is
not compatible with atomic<>. You have to use raw pointers with atomic<>.
I don't agree because the standard told us: 1.) 'For any object of trivially copyable type T, whether or not the object holds a valid value of type T, the underlying bytes making up the object can be copied into an array of char or unsigned char. If the content of the array of char or unsigned char is copied back into the object, the object shall subsequently hold its original value.' #define N sizeof(T) char buf[N]; T obj; std::memcpy(buf, &obj, N); std::memcpy(&obj, buf, N); this is correct for intrusive_ptr<T>. 2.) 'For any trivially copyable type T, if two pointers to T point to distinct T objects obj1 and obj2, where neither obj1 nor obj2 is a base-class subobject, if the underlying bytes (1.7) making up obj1 are copied into obj2,41 obj2 shall subsequently hold the same value as obj1.' T* t1p; T* t2p; std::memcpy(t1p, t2p, sizeof(T)); this is correct for intrusive_ptr<T> too. The only open question is: does the use-counter semantics prevent using intrusive_ptr<T> together with atomic?. you could increment the use-counter before and decrement the use-counter after using it with atomic<>.
On Fri, Oct 25, 2013 at 9:11 AM, Oliver Kowalke
2013/10/25 Andrey Semashev
intrusive_ptr is not trivial-copyable, precisely because it has to operate on the counter. Using memcpy for copying it is not correct.
For this reason it is
not compatible with atomic<>. You have to use raw pointers with atomic<>.
I don't agree because the standard told us:
1.) 'For any object of trivially copyable type T, whether or not the object holds a valid value of type T, the underlying bytes making up the object can be copied into an array of char or unsigned char. If the content of the array of char or unsigned char is copied back into the object, the object shall subsequently hold its original value.'
[...]
2.) 'For any trivially copyable type T, if two pointers to T point to distinct T objects obj1 and obj2, where neither obj1 nor obj2 is a base-class subobject, if the underlying bytes (1.7) making up obj1 are copied into obj2,41 obj2 shall subsequently hold the same value as obj1.'
Intrusive_ptr doesn't have tivial copy constructor (how could it have? it needs to update the counter), ergo is not trivially copyable and the above two point do not apply. -- gpd
2013/10/25 Giovanni Piero Deretta
Intrusive_ptr doesn't have tivial copy constructor (how could it have? it needs to update the counter), ergo is not trivially copyable and the above two point do not apply.
but the standard says 'trivially copyable' not 'trivial copy constructor' - e.g. the code examples by the standard document gives correct results for intrusive_ptr (because all criteria are fulfilled). does this mean the standard document contains a false wording/examples?
On Fri, Oct 25, 2013 at 10:22 AM, Oliver Kowalke
2013/10/25 Giovanni Piero Deretta
Intrusive_ptr doesn't have tivial copy constructor (how could it have? it needs to update the counter), ergo is not trivially copyable and the above two point do not apply.
but the standard says 'trivially copyable' not 'trivial copy constructor' - e.g. the code examples by the standard document gives correct results for intrusive_ptr (because all criteria are fulfilled). does this mean the standard document contains a false wording/examples?
I do not have the standard at hand, but a trivially copyable class is a class that: 1. Has no non-trivial copy constructors (this also requires no virtual functions or virtual bases) 2. Has no non-trivial move constructors 3. Has no non-trivial copy assignment operators 4. Has no non-trivial move assignment operators 5. Has a trivial destructor [from cppreference.com, which may or may not be authoritative] -- gpd
On Friday 25 October 2013 10:25:16 Giovanni Piero Deretta wrote:
On Fri, Oct 25, 2013 at 10:22 AM, Oliver Kowalke
wrote: 2013/10/25 Giovanni Piero Deretta
Intrusive_ptr doesn't have tivial copy constructor (how could it have? it needs to update the counter), ergo is not trivially copyable and the
above
two point do not apply.
but the standard says 'trivially copyable' not 'trivial copy constructor' - e.g. the code examples by the standard document gives correct results for intrusive_ptr (because all criteria are fulfilled). does this mean the standard document contains a false wording/examples?
I do not have the standard at hand, but a trivially copyable class is a class that: 1. Has no non-trivial copy constructors (this also requires no virtual functions or virtual bases)
2. Has no non-trivial move constructors
3. Has no non-trivial copy assignment operators
4. Has no non-trivial move assignment operators
5. Has a trivial destructor
[from cppreference.com, which may or may not be authoritative]
You are correct, I'll just provide some references: Trivially copyable types are defined in 3.9/9 [basic.types]. [...] Scalar types, trivially copyable class types (Clause 9), arrays of such types, and cv-qualified versions of these types (3.9.3) are collectively called trivially copyable types. [...] Trivially copyable class types are defined in 9/6 [class]. A trivially copyable class is a class that: — has no non-trivial copy constructors (12.8), — has no non-trivial move constructors (12.8), — has no non-trivial copy assignment operators (13.5.3, 12.8), — has no non-trivial move assignment operators (13.5.3, 12.8), and — has a trivial destructor (12.4). A trivial class is a class that has a trivial default constructor (12.1) and is trivially copyable. [ Note: In particular, a trivially copyable or trivial class does not have virtual functions or virtual base classes. — end note ]
2013/10/25 Giovanni Piero Deretta
On Fri, Oct 25, 2013 at 11:48 AM, Oliver Kowalke
2013/10/25 Giovanni Piero Deretta
I'm referring to standard doc at http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf Section 3.9. Types -> the example code works for intrusive_ptr too.
Well, no, the point is that it won't: char buf[N]; T obj; std::memcpy(buf, &obj, N); std::memcpy(&obj, buf, N); For T = boost::intrusive_ptr<T2>, the above code formally leads to UB, as intrusive_ptr is not trivially copiable. It might (or not) work in practice, but in the case of std::atomic, it will bypass the copy constructor which means that the reference count won't be updated. -- gpd
2013/10/25 Giovanni Piero Deretta
char buf[N]; T obj; std::memcpy(buf, &obj, N); std::memcpy(&obj, buf, N);
For T = boost::intrusive_ptr<T2>, the above code formally leads to UB, as intrusive_ptr is not trivially copiable.
It might (or not) work in practice, but in the case of std::atomic, it will bypass the copy constructor which means that the reference count won't be updated.
the use-count can be 'adjusted' before, e.g. prevent deallocating the object owned by the intrusive_ptr unintentionally. I'm interested in atomically exchange intrusive-pointers - but anyway I could also use raw-pointers in atomic<> and increment the use-counter of the intrusive_ptr owning the raw-pointer before.
On Fri, Oct 25, 2013 at 12:10 PM, Oliver Kowalke
2013/10/25 Giovanni Piero Deretta
char buf[N]; T obj; std::memcpy(buf, &obj, N); std::memcpy(&obj, buf, N);
For T = boost::intrusive_ptr<T2>, the above code formally leads to UB, as intrusive_ptr is not trivially copiable.
It might (or not) work in practice, but in the case of std::atomic, it will bypass the copy constructor which means that the reference count won't be updated.
the use-count can be 'adjusted' before, e.g. prevent deallocating the object owned by the intrusive_ptr unintentionally.
that's the tricky part. To adjust the reference count, you must load and dereference the pointer itself, but between those two operations another thread might come in, replace the pointer, adjust the original pointer count down and free the pointed object, together with the counter. You need some way to defer destruction till a safe point (RCU, hazard pointers, etc). -- gpd
2013/10/25 Giovanni Piero Deretta
that's the tricky part. To adjust the reference count, you must load and dereference the pointer itself, but between those two operations another thread might come in, replace the pointer, adjust the original pointer count down and free the pointed object, together with the counter. You need some way to defer destruction till a safe point (RCU, hazard pointers, etc).
what about this // use-counter in T is an atomic too array< atomic< intrusive_ptr< T > >, 100 > a; intrusive_ptr< T > p(...); intrusive_ptr_add_ref( p.get() ); // add an intrusive_ptr to the array if ( a[index].compare_exchange_strong(null_ptr,p) ) { // successful added } // remove an interusive_ptr from array intrusive_ptr< T > e; // points to nothing if ( ! a[index].compare_exchange_strong(e, null_ptr) ) { // successfull removed intrusive_ptr_release( e.get() ); }
On Fri, Oct 25, 2013 at 12:43 PM, Oliver Kowalke
2013/10/25 Giovanni Piero Deretta
that's the tricky part. To adjust the reference count, you must load and dereference the pointer itself, but between those two operations another thread might come in, replace the pointer, adjust the original pointer count down and free the pointed object, together with the counter. You need some way to defer destruction till a safe point (RCU, hazard pointers, etc).
what about this
// use-counter in T is an atomic too array< atomic< intrusive_ptr< T > >, 100 > a;
intrusive_ptr< T > p(...); intrusive_ptr_add_ref( p.get() ); // add an intrusive_ptr to the array if ( a[index].compare_exchange_strong(null_ptr,p) ) { // successful added }
// remove an interusive_ptr from array intrusive_ptr< T > e; // points to nothing if ( ! a[index].compare_exchange_strong(e, null_ptr) ) { // successfull removed intrusive_ptr_release( e.get() ); }
Nice! If these produce and consume are the only two operations on the array, from a cursory look, it should work fine. For the producer, a simple store with release semantics might actually be enough. The reason these operations should work is that they are actually move operations and do not change the count itself. What you can't do is copy a value out of the array leaving the original in place. -- gpd
On Fri, Oct 25, 2013 at 1:12 PM, Giovanni Piero Deretta wrote: On Fri, Oct 25, 2013 at 12:43 PM, Oliver Kowalke wrote: 2013/10/25 Giovanni Piero Deretta that's the tricky part. To adjust the reference count, you must load and
dereference the pointer itself, but between those two operations another
thread might come in, replace the pointer, adjust the original pointer
count down and free the pointed object, together with the counter. You
need
some way to defer destruction till a safe point (RCU, hazard pointers,
etc). what about this // use-counter in T is an atomic too
array< atomic< intrusive_ptr< T > >, 100 > a; intrusive_ptr< T > p(...);
intrusive_ptr_add_ref( p.get() );
// add an intrusive_ptr to the array
if ( a[index].compare_exchange_strong(null_ptr,p) ) {
// successful added
} // remove an interusive_ptr from array
intrusive_ptr< T > e; // points to nothing
if ( ! a[index].compare_exchange_strong(e, null_ptr) ) {
// successfull removed
intrusive_ptr_release( e.get() );
} [...] For the producer, a simple store with release semantics might actually be
enough. No, of course not, you can store only if there is already a null pointer
(or combine the produce/consume in a swap operation), so you need a CAS.
-- gpd
On Friday 25 October 2013 13:43:38 Oliver Kowalke wrote:
2013/10/25 Giovanni Piero Deretta
that's the tricky part. To adjust the reference count, you must load and dereference the pointer itself, but between those two operations another thread might come in, replace the pointer, adjust the original pointer count down and free the pointed object, together with the counter. You need some way to defer destruction till a safe point (RCU, hazard pointers, etc).
what about this
// use-counter in T is an atomic too array< atomic< intrusive_ptr< T > >, 100 > a;
intrusive_ptr< T > p(...); intrusive_ptr_add_ref( p.get() ); // add an intrusive_ptr to the array if ( a[index].compare_exchange_strong(null_ptr,p) ) { // successful added }
// remove an interusive_ptr from array intrusive_ptr< T > e; // points to nothing if ( ! a[index].compare_exchange_strong(e, null_ptr) ) { // successfull removed intrusive_ptr_release( e.get() ); }
That won't work for several reasons. First, as outlined before, you can't store intrusive_ptr in atomic<>. Assuming you store T* instead, then you can't use nullptr as the first argument because CAS accepts it as an lvalue reference. Next, the removal of the pointer is not correct because when CAS fails the pointer is just loaded from the atomic, so you release the pointer and the dangling pointer is left in the array. You have to load e first, then make a loop with CAS and only release if it succeeds and the loaded pointer is not NULL.
2013/10/25 Andrey Semashev
That won't work for several reasons. First, as outlined before, you can't store intrusive_ptr in atomic<>. Assuming you store T* instead, then you can't use nullptr as the first argument because CAS accepts it as an lvalue reference. Next, the removal of the pointer is not correct because when CAS fails the pointer is just loaded from the atomic, so you release the pointer and the dangling pointer is left in the array. You have to load e first, then make a loop with CAS and only release if it succeeds and the loaded pointer is not NULL.
array< atomic< T * >, 100 > a{ atomic< T* >( 0) }; T * desired = new T(); T * expected = 0; if ( a[index].compare_exchange_strong( expected, desired) ) { // desired added } T * expected1 = 0; T * expected2 = 0; if ( ! a[index].compare_exchange_strong( expected1, expected2) ) { // expected1 is not NULL, expected1 removed == a[index] contains NULL } should be OK
On Friday 25 October 2013 14:35:39 Oliver Kowalke wrote:
2013/10/25 Andrey Semashev
That won't work for several reasons. First, as outlined before, you can't store intrusive_ptr in atomic<>. Assuming you store T* instead, then you can't use nullptr as the first argument because CAS accepts it as an lvalue reference. Next, the removal of the pointer is not correct because when CAS fails the pointer is just loaded from the atomic, so you release the pointer and the dangling pointer is left in the array. You have to load e first, then make a loop with CAS and only release if it succeeds and the loaded pointer is not NULL.
array< atomic< T * >, 100 > a{ atomic< T* >( 0) };
That's not correct, it should be: array< atomic< T * >, 100 > a; for (auto& p : a) p.store(NULL, memory_order_relaxed);
T * desired = new T(); T * expected = 0; if ( a[index].compare_exchange_strong( expected, desired) ) { // desired added }
T * expected1 = 0; T * expected2 = 0; if ( ! a[index].compare_exchange_strong( expected1, expected2) ) { // expected1 is not NULL, expected1 removed == a[index] contains NULL }
No, that's not right. If compare_exchange_strong returns false, the operation have failed and a[index] != NULL (at least, that's how compare_exchange_strong have left it). It should be: T* expected = a[index].load(memory_order_acquire); while (!a[index].compare_exchange_weak(expected, NULL, memory_order_release, memory_order_acquire)) { } if (expected) // The expected value was removed from the array else // The array element was removed by some other thread
On Friday 25 October 2013 16:50:12 you wrote:
On Friday 25 October 2013 14:35:39 Oliver Kowalke wrote:
2013/10/25 Andrey Semashev
That won't work for several reasons. First, as outlined before, you can't store intrusive_ptr in atomic<>. Assuming you store T* instead, then you can't use nullptr as the first argument because CAS accepts it as an lvalue reference. Next, the removal of the pointer is not correct because when CAS fails the pointer is just loaded from the atomic, so you release the pointer and the dangling pointer is left in the array. You have to load e first, then make a loop with CAS and only release if it succeeds and the loaded pointer is not NULL.
array< atomic< T * >, 100 > a{ atomic< T* >( 0) };
That's not correct, it should be:
array< atomic< T * >, 100 > a; for (auto& p : a) p.store(NULL, memory_order_relaxed);
T * desired = new T(); T * expected = 0; if ( a[index].compare_exchange_strong( expected, desired) ) {
// desired added
}
T * expected1 = 0; T * expected2 = 0; if ( ! a[index].compare_exchange_strong( expected1, expected2) ) {
// expected1 is not NULL, expected1 removed == a[index] contains NULL
}
No, that's not right. If compare_exchange_strong returns false, the operation have failed and a[index] != NULL (at least, that's how compare_exchange_strong have left it). It should be:
T* expected = a[index].load(memory_order_acquire); while (!a[index].compare_exchange_weak(expected, NULL, memory_order_release, memory_order_acquire)) { }
if (expected) // The expected value was removed from the array else // The array element was removed by some other thread
Actually, it can be much simpler: T* p = a[index].exchange(NULL); if (expected) // The expected value was removed from the array else // The array element was removed by some other thread
On Friday 25 October 2013 15:10:24 Oliver Kowalke wrote:
2013/10/25 Andrey Semashev
Actually, it can be much simpler:
T* p = a[index].exchange(NULL); if (expected)
// The expected value was removed from the array
else
// The array element was removed by some other thread
you mean 'if ( p)' ;^)
Yes, of course. :)
2013/10/25 Andrey Semashev
T * expected1 = 0; T * expected2 = 0; if ( ! a[index].compare_exchange_strong( expected1, expected2) ) { // expected1 is not NULL, expected1 removed == a[index] contains NULL }
No, that's not right. If compare_exchange_strong returns false, the operation have failed and a[index] != NULL (at least, that's how compare_exchange_strong have left it). It should be:
T* expected = a[index].load(memory_order_acquire); while (!a[index].compare_exchange_weak(expected, NULL, memory_order_release, memory_order_acquire)) { }
if (expected) // The expected value was removed from the array else // The array element was removed by some other thread
agreed - but shouldn't we use strong memory-order (I remember that Herb S. recommended to use memory_order_strong in one of its sessions)
On Friday 25 October 2013 15:07:36 Oliver Kowalke wrote:
2013/10/25 Andrey Semashev
T * expected1 = 0;
T * expected2 = 0; if ( ! a[index].compare_exchange_strong( expected1, expected2) ) {
// expected1 is not NULL, expected1 removed == a[index] contains NULL
}
No, that's not right. If compare_exchange_strong returns false, the operation have failed and a[index] != NULL (at least, that's how compare_exchange_strong have left it). It should be:
T* expected = a[index].load(memory_order_acquire); while (!a[index].compare_exchange_weak(expected, NULL, memory_order_release, memory_order_acquire)) { }
if (expected)
// The expected value was removed from the array
else
// The array element was removed by some other thread
agreed - but shouldn't we use strong memory-order (I remember that Herb S. recommended to use memory_order_strong in one of its sessions)
Weak CAS can spuriously fail but also can be cheaper on some platforms. Spurious failures don't cause any problems in the code above, so weak CAS is preferred.
Oliver Kowalke wrote:
what about this
// use-counter in T is an atomic too array< atomic< intrusive_ptr< T > >, 100 > a;
intrusive_ptr< T > p(...); intrusive_ptr_add_ref( p.get() ); // add an intrusive_ptr to the array if ( a[index].compare_exchange_strong(null_ptr,p) ) { // successful added }
// remove an interusive_ptr from array intrusive_ptr< T > e; // points to nothing if ( ! a[index].compare_exchange_strong(e, null_ptr) ) { // successfull removed intrusive_ptr_release( e.get() ); }
For this, you don't seem to need atomic
2013/10/25 Peter Dimov
For this, you don't seem to need atomic
though; atomic works just as well, doesn't it?
yes, I could use atomic
Oliver Kowalke wrote:
yes, I could use atomic
but the other code tracks T* in intrusive_ptr< T > and it looks a little bit awkward if I release a raw-ponter from a smart-pointer in order to store it in a container.
You're already doing manual addref/release on the pointer in the container, so it's logically treated as a raw pointer. Even if you could store an intrusive_ptr there, its additional addref/release would be wasted. The rest of the code never sees the raw pointer; the functions that store and retrieve from the container take/return intrusive_ptr.
On Fri, Oct 25, 2013 at 8:35 AM, Oliver Kowalke
Hello,
do you confirm that it is valid to store a intrusive_ptr inside an atomic?!
The details of the safety of putting an intrusive_ptr in an atomic are discussed elsethread, I just wanted to remind you here that, unless you rely on exotic deferred reclamation techniques, to safely concurrently update a shared pointer normally you need to manipulate both the pointer itself and the shared count. This means either a DCAS, transactional memory or a (spin) lock. As the former two are implemented by no or few mainstream architectures, you pretty much need a lock. That's the path taken by boost::shared_ptr for example. -- gpd
2013/10/25 Giovanni Piero Deretta
The details of the safety of putting an intrusive_ptr in an atomic are discussed elsethread, I just wanted to remind you here that, unless you rely on exotic deferred reclamation techniques, to safely concurrently update a shared pointer normally you need to manipulate both the pointer itself and the shared count.
This means either a DCAS, transactional memory or a (spin) lock. As the former two are implemented by no or few mainstream architectures, you pretty much need a lock. That's the path taken by boost::shared_ptr for example.
the intention behind my question was an algorithm for work-stealing/sharing using atomic-ops.
participants (4)
-
Andrey Semashev
-
Giovanni Piero Deretta
-
Oliver Kowalke
-
Peter Dimov