Regarding shared_ptr reference count block
I had 2 questions regarding the std::shared_ptr
control block:
(1) Regarding size:
How can I programatically find the exact size of the control block for a std::shared_ptr
?
(2) Regarding logic:
Additionally, boost::shared_ptr
mentions that they are completely lock-free with respect to changes in the control block.(Starting with Boost release 1.33.0, shared_ptr
uses a lock-free implementation on most common platforms.) I don't think std::shared_ptr
follows the same - is this planned for any future C++ version? Doesn't this also mean that boost::shared_ptr
is a better idea for multithreaded cases?
c++ multithreading c++11 shared-ptr lock-free
|
show 5 more comments
I had 2 questions regarding the std::shared_ptr
control block:
(1) Regarding size:
How can I programatically find the exact size of the control block for a std::shared_ptr
?
(2) Regarding logic:
Additionally, boost::shared_ptr
mentions that they are completely lock-free with respect to changes in the control block.(Starting with Boost release 1.33.0, shared_ptr
uses a lock-free implementation on most common platforms.) I don't think std::shared_ptr
follows the same - is this planned for any future C++ version? Doesn't this also mean that boost::shared_ptr
is a better idea for multithreaded cases?
c++ multithreading c++11 shared-ptr lock-free
I'd be happy to revise the question if there's something wrong or unclear about it?(to the person who downvoted it)
– tangy
Nov 26 '18 at 4:03
4
std::shared_ptr
is a template, and, as such, since templates must be visible to the compiler, there's nothing that stops you from investigating your compiler's implementation ofstd::shared_ptr
, and figure this out.
– Sam Varshavchik
Nov 26 '18 at 4:07
True, but I'm hoping to do so programatically if possible.
– tangy
Nov 26 '18 at 4:08
2
You can look at the implementation ofstd::shared_ptr
of your compiler. Other than that, I thinkstd::shared_ptr
just uses atomics so is "lock free" in that regard. Note that ideally, you don't share mutable date across threads at all if you can help it and in that case you'll probably want simpler ref counted pointer that doesn't require atomics or locks.
– Cubic
Nov 26 '18 at 4:09
2
@tangy There is no guarantee thatstd::shared_ptr
is implemented in term of standard atomic types either. Either way, why do you care?
– curiousguy
Nov 26 '18 at 18:11
|
show 5 more comments
I had 2 questions regarding the std::shared_ptr
control block:
(1) Regarding size:
How can I programatically find the exact size of the control block for a std::shared_ptr
?
(2) Regarding logic:
Additionally, boost::shared_ptr
mentions that they are completely lock-free with respect to changes in the control block.(Starting with Boost release 1.33.0, shared_ptr
uses a lock-free implementation on most common platforms.) I don't think std::shared_ptr
follows the same - is this planned for any future C++ version? Doesn't this also mean that boost::shared_ptr
is a better idea for multithreaded cases?
c++ multithreading c++11 shared-ptr lock-free
I had 2 questions regarding the std::shared_ptr
control block:
(1) Regarding size:
How can I programatically find the exact size of the control block for a std::shared_ptr
?
(2) Regarding logic:
Additionally, boost::shared_ptr
mentions that they are completely lock-free with respect to changes in the control block.(Starting with Boost release 1.33.0, shared_ptr
uses a lock-free implementation on most common platforms.) I don't think std::shared_ptr
follows the same - is this planned for any future C++ version? Doesn't this also mean that boost::shared_ptr
is a better idea for multithreaded cases?
c++ multithreading c++11 shared-ptr lock-free
c++ multithreading c++11 shared-ptr lock-free
edited Nov 26 '18 at 13:37
curiousguy
4,65223046
4,65223046
asked Nov 26 '18 at 3:45
tangytangy
1,029721
1,029721
I'd be happy to revise the question if there's something wrong or unclear about it?(to the person who downvoted it)
– tangy
Nov 26 '18 at 4:03
4
std::shared_ptr
is a template, and, as such, since templates must be visible to the compiler, there's nothing that stops you from investigating your compiler's implementation ofstd::shared_ptr
, and figure this out.
– Sam Varshavchik
Nov 26 '18 at 4:07
True, but I'm hoping to do so programatically if possible.
– tangy
Nov 26 '18 at 4:08
2
You can look at the implementation ofstd::shared_ptr
of your compiler. Other than that, I thinkstd::shared_ptr
just uses atomics so is "lock free" in that regard. Note that ideally, you don't share mutable date across threads at all if you can help it and in that case you'll probably want simpler ref counted pointer that doesn't require atomics or locks.
– Cubic
Nov 26 '18 at 4:09
2
@tangy There is no guarantee thatstd::shared_ptr
is implemented in term of standard atomic types either. Either way, why do you care?
– curiousguy
Nov 26 '18 at 18:11
|
show 5 more comments
I'd be happy to revise the question if there's something wrong or unclear about it?(to the person who downvoted it)
– tangy
Nov 26 '18 at 4:03
4
std::shared_ptr
is a template, and, as such, since templates must be visible to the compiler, there's nothing that stops you from investigating your compiler's implementation ofstd::shared_ptr
, and figure this out.
– Sam Varshavchik
Nov 26 '18 at 4:07
True, but I'm hoping to do so programatically if possible.
– tangy
Nov 26 '18 at 4:08
2
You can look at the implementation ofstd::shared_ptr
of your compiler. Other than that, I thinkstd::shared_ptr
just uses atomics so is "lock free" in that regard. Note that ideally, you don't share mutable date across threads at all if you can help it and in that case you'll probably want simpler ref counted pointer that doesn't require atomics or locks.
– Cubic
Nov 26 '18 at 4:09
2
@tangy There is no guarantee thatstd::shared_ptr
is implemented in term of standard atomic types either. Either way, why do you care?
– curiousguy
Nov 26 '18 at 18:11
I'd be happy to revise the question if there's something wrong or unclear about it?(to the person who downvoted it)
– tangy
Nov 26 '18 at 4:03
I'd be happy to revise the question if there's something wrong or unclear about it?(to the person who downvoted it)
– tangy
Nov 26 '18 at 4:03
4
4
std::shared_ptr
is a template, and, as such, since templates must be visible to the compiler, there's nothing that stops you from investigating your compiler's implementation of std::shared_ptr
, and figure this out.– Sam Varshavchik
Nov 26 '18 at 4:07
std::shared_ptr
is a template, and, as such, since templates must be visible to the compiler, there's nothing that stops you from investigating your compiler's implementation of std::shared_ptr
, and figure this out.– Sam Varshavchik
Nov 26 '18 at 4:07
True, but I'm hoping to do so programatically if possible.
– tangy
Nov 26 '18 at 4:08
True, but I'm hoping to do so programatically if possible.
– tangy
Nov 26 '18 at 4:08
2
2
You can look at the implementation of
std::shared_ptr
of your compiler. Other than that, I think std::shared_ptr
just uses atomics so is "lock free" in that regard. Note that ideally, you don't share mutable date across threads at all if you can help it and in that case you'll probably want simpler ref counted pointer that doesn't require atomics or locks.– Cubic
Nov 26 '18 at 4:09
You can look at the implementation of
std::shared_ptr
of your compiler. Other than that, I think std::shared_ptr
just uses atomics so is "lock free" in that regard. Note that ideally, you don't share mutable date across threads at all if you can help it and in that case you'll probably want simpler ref counted pointer that doesn't require atomics or locks.– Cubic
Nov 26 '18 at 4:09
2
2
@tangy There is no guarantee that
std::shared_ptr
is implemented in term of standard atomic types either. Either way, why do you care?– curiousguy
Nov 26 '18 at 18:11
@tangy There is no guarantee that
std::shared_ptr
is implemented in term of standard atomic types either. Either way, why do you care?– curiousguy
Nov 26 '18 at 18:11
|
show 5 more comments
3 Answers
3
active
oldest
votes
(1) Regarding size: How can I programatically find the exact size of the control block for a std::shared_ptr?
There is no way. It's not directly accessible.
(2) Regarding logic: Additionally, boost::shared_ptr mentions that they are completely lock-free with respect to changes in the control block.(Starting with Boost release 1.33.0, shared_ptr uses a lock-free implementation on most common platforms.) I don't think std::shared_ptr follows the same - is this planned for any future C++ version? Doesn't this also mean that boost::shared_ptr is a better idea for multithreaded cases?
Absolutely not. Lock-free implementations are not always better than implementations that use locks. Having an additional constraint, at best, doesn't make the implementation worse but it cannot possibly make the implementation better.
Consider two equally competent programmers each doing their best to implement shared_ptr
. One must produce a lock-free implementation. The other is completely free to use their best judgment. There is simply no way the one that must produce a lock-free implementation can produce a better implementation all other things being equal. At best, a lock-free implementation is best and they'll both produce one. At worse, on this platform a lock-free implementation has huge disadvantages and one implementer must use one. Yuck.
Do note that the Boost quote is not "completely lock-free"; it's merely lock-free "on most common platforms".
– Nicol Bolas
Nov 26 '18 at 4:30
2
@NicolBolas Yeah. It's probably just a statement about what the implementers judged to be best. It's hard to imagine how you would ever need or want a lock on any modern platform -- there's no case where any thread would ever need to wait for any other thread.
– David Schwartz
Nov 26 '18 at 4:34
3
C++11 only requires lock-freestd::atomic_flag
, which is enough to build a lock from, but not enough for lockless ref-counting. Putting a lock-freestd::shared_ptr
requirement / guarantee in the standard would theoretically restrict which platforms could support a conforming C++11 implementation. I think that's the reason, not that locking might be actually better on a normal platform where lock-free was possible.
– Peter Cordes
Nov 26 '18 at 20:57
add a comment |
The control block is not exposed. In implementations I have read it is dynamic in size to store the deleter contiguously (and/or, in the case of make shared, the object itself).
In general it contains at least 3 pointer-size fields - weak, strong count, and deleter invoker.
At least one implementation relies on RTTI; others do not.
Operations on the count use atomic operations in the implementations I have read; note that C++ does not require atomic operatins to all be lock free (I believe a platform that doesn't have pointer-size lock-free operations can be a conforming C++ platform).
Their state is are consistent with each other and themselves, but no attempt to make them consistent with object state occurs. This is why using raw shared ptrs as copy on write pImpls may be error prone on some platforms.
1
Yup, onlystd::atomic_flag
is guaranteed lock-free. Good implementations on targets that can do it will use lock-free atomics forshared_ptr
andatomic<T>
up to a width the HW can support, of course.
– Peter Cordes
Nov 26 '18 at 21:00
add a comment |
(1)
Of course it is best to check implementation, however you still may make some checks from your program.
Control block is allocated dynamically, so to determine its size you may overload new operator.
Then what you may also check is if std::make_shared provides you with some optimization of control block size.
In proper implementation I would expect that this will make two allocations (objectA and control block):
std::shared_ptr<A> i(new A());
However this will make only one allocation (and then objectA initialized with placement new):
auto a = std::make_shared<A>();
Consider following example:
#include <iostream>
#include <memory>
void * operator new(size_t size)
{
std::cout << "Requested allocation: " << size << std::endl;
void * p = malloc(size);
return p;
}
class A {};
class B
{
int a[8];
};
int main()
{
std::cout << "Sizeof int: " << sizeof(int) << ", A(empty): " << sizeof(A) << ", B(8 ints): " << sizeof(B) << std::endl;
{
std::cout << "Just new:" << std::endl;
std::cout << "- int:" << std::endl;
std::shared_ptr<int> i(new int());
std::cout << "- A(empty):" << std::endl;
std::shared_ptr<A> a(new A());
std::cout << "- B(8 ints):" << std::endl;
std::shared_ptr<B> b(new B());
}
{
std::cout << "Make shared:" << std::endl;
std::cout << "- int:" << std::endl;
auto i = std::make_shared<int>();
std::cout << "- A(empty):" << std::endl;
auto a = std::make_shared<A>();
std::cout << "- B(8 ints):" << std::endl;
auto b = std::make_shared<B>();
}
}
The output I received (of course it is hw architecture and compiler specific):
Sizeof int: 4, A(empty): 1, B(8 ints): 32
Just new:
- int:
Requested allocation: 4
Requested allocation: 24
First allocation for int - 4 bytes, next one for control block - 24 bytes.
- A(empty):
Requested allocation: 1
Requested allocation: 24
- B(8 ints):
Requested allocation: 32
Requested allocation: 24
Looks that control block is (most probably) 24 bytes.
Here is why to use make_shared:
Make shared:
- int:
Requested allocation: 24
Only one allocation, int + control block = 24 bytes, less then before.
- A(empty):
Requested allocation: 24
- B(8 ints):
Requested allocation: 48
Here one could expect 56 (32+24), but it looks that implementation is optimized. If you use make_shared - pointer to actual object is not needed in control block and its size is only 16 bytes.
Other possibility to check the size of control block is to:
std::cout<< sizeof(std::enable_shared_from_this<int>);
In my case:
16
So I would say that the size of control block in my case is 16-24 bytes, depending on how it was created.
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53474539%2fregarding-shared-ptr-reference-count-block%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
3 Answers
3
active
oldest
votes
3 Answers
3
active
oldest
votes
active
oldest
votes
active
oldest
votes
(1) Regarding size: How can I programatically find the exact size of the control block for a std::shared_ptr?
There is no way. It's not directly accessible.
(2) Regarding logic: Additionally, boost::shared_ptr mentions that they are completely lock-free with respect to changes in the control block.(Starting with Boost release 1.33.0, shared_ptr uses a lock-free implementation on most common platforms.) I don't think std::shared_ptr follows the same - is this planned for any future C++ version? Doesn't this also mean that boost::shared_ptr is a better idea for multithreaded cases?
Absolutely not. Lock-free implementations are not always better than implementations that use locks. Having an additional constraint, at best, doesn't make the implementation worse but it cannot possibly make the implementation better.
Consider two equally competent programmers each doing their best to implement shared_ptr
. One must produce a lock-free implementation. The other is completely free to use their best judgment. There is simply no way the one that must produce a lock-free implementation can produce a better implementation all other things being equal. At best, a lock-free implementation is best and they'll both produce one. At worse, on this platform a lock-free implementation has huge disadvantages and one implementer must use one. Yuck.
Do note that the Boost quote is not "completely lock-free"; it's merely lock-free "on most common platforms".
– Nicol Bolas
Nov 26 '18 at 4:30
2
@NicolBolas Yeah. It's probably just a statement about what the implementers judged to be best. It's hard to imagine how you would ever need or want a lock on any modern platform -- there's no case where any thread would ever need to wait for any other thread.
– David Schwartz
Nov 26 '18 at 4:34
3
C++11 only requires lock-freestd::atomic_flag
, which is enough to build a lock from, but not enough for lockless ref-counting. Putting a lock-freestd::shared_ptr
requirement / guarantee in the standard would theoretically restrict which platforms could support a conforming C++11 implementation. I think that's the reason, not that locking might be actually better on a normal platform where lock-free was possible.
– Peter Cordes
Nov 26 '18 at 20:57
add a comment |
(1) Regarding size: How can I programatically find the exact size of the control block for a std::shared_ptr?
There is no way. It's not directly accessible.
(2) Regarding logic: Additionally, boost::shared_ptr mentions that they are completely lock-free with respect to changes in the control block.(Starting with Boost release 1.33.0, shared_ptr uses a lock-free implementation on most common platforms.) I don't think std::shared_ptr follows the same - is this planned for any future C++ version? Doesn't this also mean that boost::shared_ptr is a better idea for multithreaded cases?
Absolutely not. Lock-free implementations are not always better than implementations that use locks. Having an additional constraint, at best, doesn't make the implementation worse but it cannot possibly make the implementation better.
Consider two equally competent programmers each doing their best to implement shared_ptr
. One must produce a lock-free implementation. The other is completely free to use their best judgment. There is simply no way the one that must produce a lock-free implementation can produce a better implementation all other things being equal. At best, a lock-free implementation is best and they'll both produce one. At worse, on this platform a lock-free implementation has huge disadvantages and one implementer must use one. Yuck.
Do note that the Boost quote is not "completely lock-free"; it's merely lock-free "on most common platforms".
– Nicol Bolas
Nov 26 '18 at 4:30
2
@NicolBolas Yeah. It's probably just a statement about what the implementers judged to be best. It's hard to imagine how you would ever need or want a lock on any modern platform -- there's no case where any thread would ever need to wait for any other thread.
– David Schwartz
Nov 26 '18 at 4:34
3
C++11 only requires lock-freestd::atomic_flag
, which is enough to build a lock from, but not enough for lockless ref-counting. Putting a lock-freestd::shared_ptr
requirement / guarantee in the standard would theoretically restrict which platforms could support a conforming C++11 implementation. I think that's the reason, not that locking might be actually better on a normal platform where lock-free was possible.
– Peter Cordes
Nov 26 '18 at 20:57
add a comment |
(1) Regarding size: How can I programatically find the exact size of the control block for a std::shared_ptr?
There is no way. It's not directly accessible.
(2) Regarding logic: Additionally, boost::shared_ptr mentions that they are completely lock-free with respect to changes in the control block.(Starting with Boost release 1.33.0, shared_ptr uses a lock-free implementation on most common platforms.) I don't think std::shared_ptr follows the same - is this planned for any future C++ version? Doesn't this also mean that boost::shared_ptr is a better idea for multithreaded cases?
Absolutely not. Lock-free implementations are not always better than implementations that use locks. Having an additional constraint, at best, doesn't make the implementation worse but it cannot possibly make the implementation better.
Consider two equally competent programmers each doing their best to implement shared_ptr
. One must produce a lock-free implementation. The other is completely free to use their best judgment. There is simply no way the one that must produce a lock-free implementation can produce a better implementation all other things being equal. At best, a lock-free implementation is best and they'll both produce one. At worse, on this platform a lock-free implementation has huge disadvantages and one implementer must use one. Yuck.
(1) Regarding size: How can I programatically find the exact size of the control block for a std::shared_ptr?
There is no way. It's not directly accessible.
(2) Regarding logic: Additionally, boost::shared_ptr mentions that they are completely lock-free with respect to changes in the control block.(Starting with Boost release 1.33.0, shared_ptr uses a lock-free implementation on most common platforms.) I don't think std::shared_ptr follows the same - is this planned for any future C++ version? Doesn't this also mean that boost::shared_ptr is a better idea for multithreaded cases?
Absolutely not. Lock-free implementations are not always better than implementations that use locks. Having an additional constraint, at best, doesn't make the implementation worse but it cannot possibly make the implementation better.
Consider two equally competent programmers each doing their best to implement shared_ptr
. One must produce a lock-free implementation. The other is completely free to use their best judgment. There is simply no way the one that must produce a lock-free implementation can produce a better implementation all other things being equal. At best, a lock-free implementation is best and they'll both produce one. At worse, on this platform a lock-free implementation has huge disadvantages and one implementer must use one. Yuck.
answered Nov 26 '18 at 4:26
David SchwartzDavid Schwartz
139k14145230
139k14145230
Do note that the Boost quote is not "completely lock-free"; it's merely lock-free "on most common platforms".
– Nicol Bolas
Nov 26 '18 at 4:30
2
@NicolBolas Yeah. It's probably just a statement about what the implementers judged to be best. It's hard to imagine how you would ever need or want a lock on any modern platform -- there's no case where any thread would ever need to wait for any other thread.
– David Schwartz
Nov 26 '18 at 4:34
3
C++11 only requires lock-freestd::atomic_flag
, which is enough to build a lock from, but not enough for lockless ref-counting. Putting a lock-freestd::shared_ptr
requirement / guarantee in the standard would theoretically restrict which platforms could support a conforming C++11 implementation. I think that's the reason, not that locking might be actually better on a normal platform where lock-free was possible.
– Peter Cordes
Nov 26 '18 at 20:57
add a comment |
Do note that the Boost quote is not "completely lock-free"; it's merely lock-free "on most common platforms".
– Nicol Bolas
Nov 26 '18 at 4:30
2
@NicolBolas Yeah. It's probably just a statement about what the implementers judged to be best. It's hard to imagine how you would ever need or want a lock on any modern platform -- there's no case where any thread would ever need to wait for any other thread.
– David Schwartz
Nov 26 '18 at 4:34
3
C++11 only requires lock-freestd::atomic_flag
, which is enough to build a lock from, but not enough for lockless ref-counting. Putting a lock-freestd::shared_ptr
requirement / guarantee in the standard would theoretically restrict which platforms could support a conforming C++11 implementation. I think that's the reason, not that locking might be actually better on a normal platform where lock-free was possible.
– Peter Cordes
Nov 26 '18 at 20:57
Do note that the Boost quote is not "completely lock-free"; it's merely lock-free "on most common platforms".
– Nicol Bolas
Nov 26 '18 at 4:30
Do note that the Boost quote is not "completely lock-free"; it's merely lock-free "on most common platforms".
– Nicol Bolas
Nov 26 '18 at 4:30
2
2
@NicolBolas Yeah. It's probably just a statement about what the implementers judged to be best. It's hard to imagine how you would ever need or want a lock on any modern platform -- there's no case where any thread would ever need to wait for any other thread.
– David Schwartz
Nov 26 '18 at 4:34
@NicolBolas Yeah. It's probably just a statement about what the implementers judged to be best. It's hard to imagine how you would ever need or want a lock on any modern platform -- there's no case where any thread would ever need to wait for any other thread.
– David Schwartz
Nov 26 '18 at 4:34
3
3
C++11 only requires lock-free
std::atomic_flag
, which is enough to build a lock from, but not enough for lockless ref-counting. Putting a lock-free std::shared_ptr
requirement / guarantee in the standard would theoretically restrict which platforms could support a conforming C++11 implementation. I think that's the reason, not that locking might be actually better on a normal platform where lock-free was possible.– Peter Cordes
Nov 26 '18 at 20:57
C++11 only requires lock-free
std::atomic_flag
, which is enough to build a lock from, but not enough for lockless ref-counting. Putting a lock-free std::shared_ptr
requirement / guarantee in the standard would theoretically restrict which platforms could support a conforming C++11 implementation. I think that's the reason, not that locking might be actually better on a normal platform where lock-free was possible.– Peter Cordes
Nov 26 '18 at 20:57
add a comment |
The control block is not exposed. In implementations I have read it is dynamic in size to store the deleter contiguously (and/or, in the case of make shared, the object itself).
In general it contains at least 3 pointer-size fields - weak, strong count, and deleter invoker.
At least one implementation relies on RTTI; others do not.
Operations on the count use atomic operations in the implementations I have read; note that C++ does not require atomic operatins to all be lock free (I believe a platform that doesn't have pointer-size lock-free operations can be a conforming C++ platform).
Their state is are consistent with each other and themselves, but no attempt to make them consistent with object state occurs. This is why using raw shared ptrs as copy on write pImpls may be error prone on some platforms.
1
Yup, onlystd::atomic_flag
is guaranteed lock-free. Good implementations on targets that can do it will use lock-free atomics forshared_ptr
andatomic<T>
up to a width the HW can support, of course.
– Peter Cordes
Nov 26 '18 at 21:00
add a comment |
The control block is not exposed. In implementations I have read it is dynamic in size to store the deleter contiguously (and/or, in the case of make shared, the object itself).
In general it contains at least 3 pointer-size fields - weak, strong count, and deleter invoker.
At least one implementation relies on RTTI; others do not.
Operations on the count use atomic operations in the implementations I have read; note that C++ does not require atomic operatins to all be lock free (I believe a platform that doesn't have pointer-size lock-free operations can be a conforming C++ platform).
Their state is are consistent with each other and themselves, but no attempt to make them consistent with object state occurs. This is why using raw shared ptrs as copy on write pImpls may be error prone on some platforms.
1
Yup, onlystd::atomic_flag
is guaranteed lock-free. Good implementations on targets that can do it will use lock-free atomics forshared_ptr
andatomic<T>
up to a width the HW can support, of course.
– Peter Cordes
Nov 26 '18 at 21:00
add a comment |
The control block is not exposed. In implementations I have read it is dynamic in size to store the deleter contiguously (and/or, in the case of make shared, the object itself).
In general it contains at least 3 pointer-size fields - weak, strong count, and deleter invoker.
At least one implementation relies on RTTI; others do not.
Operations on the count use atomic operations in the implementations I have read; note that C++ does not require atomic operatins to all be lock free (I believe a platform that doesn't have pointer-size lock-free operations can be a conforming C++ platform).
Their state is are consistent with each other and themselves, but no attempt to make them consistent with object state occurs. This is why using raw shared ptrs as copy on write pImpls may be error prone on some platforms.
The control block is not exposed. In implementations I have read it is dynamic in size to store the deleter contiguously (and/or, in the case of make shared, the object itself).
In general it contains at least 3 pointer-size fields - weak, strong count, and deleter invoker.
At least one implementation relies on RTTI; others do not.
Operations on the count use atomic operations in the implementations I have read; note that C++ does not require atomic operatins to all be lock free (I believe a platform that doesn't have pointer-size lock-free operations can be a conforming C++ platform).
Their state is are consistent with each other and themselves, but no attempt to make them consistent with object state occurs. This is why using raw shared ptrs as copy on write pImpls may be error prone on some platforms.
answered Nov 26 '18 at 4:23
Yakk - Adam NevraumontYakk - Adam Nevraumont
188k21199384
188k21199384
1
Yup, onlystd::atomic_flag
is guaranteed lock-free. Good implementations on targets that can do it will use lock-free atomics forshared_ptr
andatomic<T>
up to a width the HW can support, of course.
– Peter Cordes
Nov 26 '18 at 21:00
add a comment |
1
Yup, onlystd::atomic_flag
is guaranteed lock-free. Good implementations on targets that can do it will use lock-free atomics forshared_ptr
andatomic<T>
up to a width the HW can support, of course.
– Peter Cordes
Nov 26 '18 at 21:00
1
1
Yup, only
std::atomic_flag
is guaranteed lock-free. Good implementations on targets that can do it will use lock-free atomics for shared_ptr
and atomic<T>
up to a width the HW can support, of course.– Peter Cordes
Nov 26 '18 at 21:00
Yup, only
std::atomic_flag
is guaranteed lock-free. Good implementations on targets that can do it will use lock-free atomics for shared_ptr
and atomic<T>
up to a width the HW can support, of course.– Peter Cordes
Nov 26 '18 at 21:00
add a comment |
(1)
Of course it is best to check implementation, however you still may make some checks from your program.
Control block is allocated dynamically, so to determine its size you may overload new operator.
Then what you may also check is if std::make_shared provides you with some optimization of control block size.
In proper implementation I would expect that this will make two allocations (objectA and control block):
std::shared_ptr<A> i(new A());
However this will make only one allocation (and then objectA initialized with placement new):
auto a = std::make_shared<A>();
Consider following example:
#include <iostream>
#include <memory>
void * operator new(size_t size)
{
std::cout << "Requested allocation: " << size << std::endl;
void * p = malloc(size);
return p;
}
class A {};
class B
{
int a[8];
};
int main()
{
std::cout << "Sizeof int: " << sizeof(int) << ", A(empty): " << sizeof(A) << ", B(8 ints): " << sizeof(B) << std::endl;
{
std::cout << "Just new:" << std::endl;
std::cout << "- int:" << std::endl;
std::shared_ptr<int> i(new int());
std::cout << "- A(empty):" << std::endl;
std::shared_ptr<A> a(new A());
std::cout << "- B(8 ints):" << std::endl;
std::shared_ptr<B> b(new B());
}
{
std::cout << "Make shared:" << std::endl;
std::cout << "- int:" << std::endl;
auto i = std::make_shared<int>();
std::cout << "- A(empty):" << std::endl;
auto a = std::make_shared<A>();
std::cout << "- B(8 ints):" << std::endl;
auto b = std::make_shared<B>();
}
}
The output I received (of course it is hw architecture and compiler specific):
Sizeof int: 4, A(empty): 1, B(8 ints): 32
Just new:
- int:
Requested allocation: 4
Requested allocation: 24
First allocation for int - 4 bytes, next one for control block - 24 bytes.
- A(empty):
Requested allocation: 1
Requested allocation: 24
- B(8 ints):
Requested allocation: 32
Requested allocation: 24
Looks that control block is (most probably) 24 bytes.
Here is why to use make_shared:
Make shared:
- int:
Requested allocation: 24
Only one allocation, int + control block = 24 bytes, less then before.
- A(empty):
Requested allocation: 24
- B(8 ints):
Requested allocation: 48
Here one could expect 56 (32+24), but it looks that implementation is optimized. If you use make_shared - pointer to actual object is not needed in control block and its size is only 16 bytes.
Other possibility to check the size of control block is to:
std::cout<< sizeof(std::enable_shared_from_this<int>);
In my case:
16
So I would say that the size of control block in my case is 16-24 bytes, depending on how it was created.
add a comment |
(1)
Of course it is best to check implementation, however you still may make some checks from your program.
Control block is allocated dynamically, so to determine its size you may overload new operator.
Then what you may also check is if std::make_shared provides you with some optimization of control block size.
In proper implementation I would expect that this will make two allocations (objectA and control block):
std::shared_ptr<A> i(new A());
However this will make only one allocation (and then objectA initialized with placement new):
auto a = std::make_shared<A>();
Consider following example:
#include <iostream>
#include <memory>
void * operator new(size_t size)
{
std::cout << "Requested allocation: " << size << std::endl;
void * p = malloc(size);
return p;
}
class A {};
class B
{
int a[8];
};
int main()
{
std::cout << "Sizeof int: " << sizeof(int) << ", A(empty): " << sizeof(A) << ", B(8 ints): " << sizeof(B) << std::endl;
{
std::cout << "Just new:" << std::endl;
std::cout << "- int:" << std::endl;
std::shared_ptr<int> i(new int());
std::cout << "- A(empty):" << std::endl;
std::shared_ptr<A> a(new A());
std::cout << "- B(8 ints):" << std::endl;
std::shared_ptr<B> b(new B());
}
{
std::cout << "Make shared:" << std::endl;
std::cout << "- int:" << std::endl;
auto i = std::make_shared<int>();
std::cout << "- A(empty):" << std::endl;
auto a = std::make_shared<A>();
std::cout << "- B(8 ints):" << std::endl;
auto b = std::make_shared<B>();
}
}
The output I received (of course it is hw architecture and compiler specific):
Sizeof int: 4, A(empty): 1, B(8 ints): 32
Just new:
- int:
Requested allocation: 4
Requested allocation: 24
First allocation for int - 4 bytes, next one for control block - 24 bytes.
- A(empty):
Requested allocation: 1
Requested allocation: 24
- B(8 ints):
Requested allocation: 32
Requested allocation: 24
Looks that control block is (most probably) 24 bytes.
Here is why to use make_shared:
Make shared:
- int:
Requested allocation: 24
Only one allocation, int + control block = 24 bytes, less then before.
- A(empty):
Requested allocation: 24
- B(8 ints):
Requested allocation: 48
Here one could expect 56 (32+24), but it looks that implementation is optimized. If you use make_shared - pointer to actual object is not needed in control block and its size is only 16 bytes.
Other possibility to check the size of control block is to:
std::cout<< sizeof(std::enable_shared_from_this<int>);
In my case:
16
So I would say that the size of control block in my case is 16-24 bytes, depending on how it was created.
add a comment |
(1)
Of course it is best to check implementation, however you still may make some checks from your program.
Control block is allocated dynamically, so to determine its size you may overload new operator.
Then what you may also check is if std::make_shared provides you with some optimization of control block size.
In proper implementation I would expect that this will make two allocations (objectA and control block):
std::shared_ptr<A> i(new A());
However this will make only one allocation (and then objectA initialized with placement new):
auto a = std::make_shared<A>();
Consider following example:
#include <iostream>
#include <memory>
void * operator new(size_t size)
{
std::cout << "Requested allocation: " << size << std::endl;
void * p = malloc(size);
return p;
}
class A {};
class B
{
int a[8];
};
int main()
{
std::cout << "Sizeof int: " << sizeof(int) << ", A(empty): " << sizeof(A) << ", B(8 ints): " << sizeof(B) << std::endl;
{
std::cout << "Just new:" << std::endl;
std::cout << "- int:" << std::endl;
std::shared_ptr<int> i(new int());
std::cout << "- A(empty):" << std::endl;
std::shared_ptr<A> a(new A());
std::cout << "- B(8 ints):" << std::endl;
std::shared_ptr<B> b(new B());
}
{
std::cout << "Make shared:" << std::endl;
std::cout << "- int:" << std::endl;
auto i = std::make_shared<int>();
std::cout << "- A(empty):" << std::endl;
auto a = std::make_shared<A>();
std::cout << "- B(8 ints):" << std::endl;
auto b = std::make_shared<B>();
}
}
The output I received (of course it is hw architecture and compiler specific):
Sizeof int: 4, A(empty): 1, B(8 ints): 32
Just new:
- int:
Requested allocation: 4
Requested allocation: 24
First allocation for int - 4 bytes, next one for control block - 24 bytes.
- A(empty):
Requested allocation: 1
Requested allocation: 24
- B(8 ints):
Requested allocation: 32
Requested allocation: 24
Looks that control block is (most probably) 24 bytes.
Here is why to use make_shared:
Make shared:
- int:
Requested allocation: 24
Only one allocation, int + control block = 24 bytes, less then before.
- A(empty):
Requested allocation: 24
- B(8 ints):
Requested allocation: 48
Here one could expect 56 (32+24), but it looks that implementation is optimized. If you use make_shared - pointer to actual object is not needed in control block and its size is only 16 bytes.
Other possibility to check the size of control block is to:
std::cout<< sizeof(std::enable_shared_from_this<int>);
In my case:
16
So I would say that the size of control block in my case is 16-24 bytes, depending on how it was created.
(1)
Of course it is best to check implementation, however you still may make some checks from your program.
Control block is allocated dynamically, so to determine its size you may overload new operator.
Then what you may also check is if std::make_shared provides you with some optimization of control block size.
In proper implementation I would expect that this will make two allocations (objectA and control block):
std::shared_ptr<A> i(new A());
However this will make only one allocation (and then objectA initialized with placement new):
auto a = std::make_shared<A>();
Consider following example:
#include <iostream>
#include <memory>
void * operator new(size_t size)
{
std::cout << "Requested allocation: " << size << std::endl;
void * p = malloc(size);
return p;
}
class A {};
class B
{
int a[8];
};
int main()
{
std::cout << "Sizeof int: " << sizeof(int) << ", A(empty): " << sizeof(A) << ", B(8 ints): " << sizeof(B) << std::endl;
{
std::cout << "Just new:" << std::endl;
std::cout << "- int:" << std::endl;
std::shared_ptr<int> i(new int());
std::cout << "- A(empty):" << std::endl;
std::shared_ptr<A> a(new A());
std::cout << "- B(8 ints):" << std::endl;
std::shared_ptr<B> b(new B());
}
{
std::cout << "Make shared:" << std::endl;
std::cout << "- int:" << std::endl;
auto i = std::make_shared<int>();
std::cout << "- A(empty):" << std::endl;
auto a = std::make_shared<A>();
std::cout << "- B(8 ints):" << std::endl;
auto b = std::make_shared<B>();
}
}
The output I received (of course it is hw architecture and compiler specific):
Sizeof int: 4, A(empty): 1, B(8 ints): 32
Just new:
- int:
Requested allocation: 4
Requested allocation: 24
First allocation for int - 4 bytes, next one for control block - 24 bytes.
- A(empty):
Requested allocation: 1
Requested allocation: 24
- B(8 ints):
Requested allocation: 32
Requested allocation: 24
Looks that control block is (most probably) 24 bytes.
Here is why to use make_shared:
Make shared:
- int:
Requested allocation: 24
Only one allocation, int + control block = 24 bytes, less then before.
- A(empty):
Requested allocation: 24
- B(8 ints):
Requested allocation: 48
Here one could expect 56 (32+24), but it looks that implementation is optimized. If you use make_shared - pointer to actual object is not needed in control block and its size is only 16 bytes.
Other possibility to check the size of control block is to:
std::cout<< sizeof(std::enable_shared_from_this<int>);
In my case:
16
So I would say that the size of control block in my case is 16-24 bytes, depending on how it was created.
answered Dec 22 '18 at 21:04
wpfmanwpfman
211
211
add a comment |
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53474539%2fregarding-shared-ptr-reference-count-block%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
I'd be happy to revise the question if there's something wrong or unclear about it?(to the person who downvoted it)
– tangy
Nov 26 '18 at 4:03
4
std::shared_ptr
is a template, and, as such, since templates must be visible to the compiler, there's nothing that stops you from investigating your compiler's implementation ofstd::shared_ptr
, and figure this out.– Sam Varshavchik
Nov 26 '18 at 4:07
True, but I'm hoping to do so programatically if possible.
– tangy
Nov 26 '18 at 4:08
2
You can look at the implementation of
std::shared_ptr
of your compiler. Other than that, I thinkstd::shared_ptr
just uses atomics so is "lock free" in that regard. Note that ideally, you don't share mutable date across threads at all if you can help it and in that case you'll probably want simpler ref counted pointer that doesn't require atomics or locks.– Cubic
Nov 26 '18 at 4:09
2
@tangy There is no guarantee that
std::shared_ptr
is implemented in term of standard atomic types either. Either way, why do you care?– curiousguy
Nov 26 '18 at 18:11