Thread-safe reference counts.

A

Alexander Terekhov

Jon Harrop wrote:
[...]
I'm amazed anyone would bother putting a really poor GC in C++ rather than

using a more appropriate language but, IMHO, C++0x is the next Fortran 2003
anyway...

Agreed, regarding the Fortran part. :)

Hey, "The COBOL 2002 standard includes support for object-oriented
programming and other modern language features." ;-)

regards,
alexander.
 
C

Chris Thomasson

Just to make it clear: the original posting talked about memory
management, not object lifetime management. From the way it was
presented, it more or less sounded like object lifetime had no
significance (often the case). In which case, garbage
collection is fine. But garbage collection has nothing to do
with object lifetime (which, when significant, still must be
managed, garbage collection or not).

GC does indeed detect when an object is not being referenced by anything
(e.g., in a quiescent state). IMHO, this is a _very_ import part of lifetime
management because you cannot terminate an objects life if its being used by
something else.

[...]
 
J

Jon Harrop

Alexander said:
Jon Harrop wrote:
[...]
I'm amazed anyone would bother putting a really poor GC in C++ rather
than

shared_ptr<> isn't really a GC. And if you view it from the GC angle, it
is indeed really poor.

What does it do apart from collect garbage?
 
C

Chris Thomasson

Jon Harrop said:
Read Jones and Lins "Garbage Collection" and references therein for a
start.

Reference counting is very slow because updating reference counts requires
cache incoherent memory access, which is very slow on modern hardware and
is getting relatively slower every year as CPU speed increases faster than
memory speed. The slowdown is already enormous.

Let me ask you again: What counting algorithms are you talking about? Did
you know that some algorithms do not need to perform atomic operations or
memory-barriers in order to update a counter?
 
J

Jon Harrop

Chris said:
Let me ask you again: What counting algorithms are you talking about? Did
you know that some algorithms do not need to perform atomic operations or
memory-barriers in order to update a counter?

Anything counting references. Memory barriers simply worsen the situation
even more.
 
C

Chris Thomasson

[added comp.programming.threads]

Well, they obviously leak if cycles are present. And while "an
order of magnitude slower" is obviously false as well, Hans
Boehm has performed a number of benchmarks, and they are slower
in a lot of cases.

You can definitely misuse reference counting. For instance, you would NOT
want to use it if you were iterating over a large linked-lists because you
would have to update the counter on each object visited during the
traversal. Take a lock-free reader/writer pattern based on atomic pointers
into account:

<code-sketch>
_____________________________________________________________________
struct object {
atomic_ptr<object> m_next;
void read_only_operation() const;
};


static atomic_ptr<object> g_head;


void writer_push() {
local_ptr<object> l_obj(new object);
local_ptr<object> l_cmp(g_head);
do {
l_obj->m_next = l_cmp;
} while (! g_head.cas(l_cmp, l_obj));
}


void writer_pop() {
local_ptr<object> l_cmp(g_head);
do {
if (! l_cmp) { return; }
} while (! g_head.cas(l_cmp, l_cmp->m_next));
}


void readers() {
local_ptr<object> l_obj(g_head);
while (l_obj) {
l_obj->read_only_operation();
l_obj = l_obj->m_next;
}
}
_____________________________________________________________________


That would give you horrible performance on the readers _and_ the writers.
However, there is a solution. You can use reference counting to manage
so-called proxy collector objects. I can rewrite the code above using a
proxy garbage collector. I will use this implementation for the refactored
code:

http://appcore.home.comcast.net/misc/pc_sample_h_v1.html


Now I can write the above as:
_____________________________________________________________________
struct object {
object* m_next;
pc_node m_pcn;

object() : m_next(NULL) {
pc_node_init(&m_pcn);
}

void read_only_operation() const;
};


static object_dtor(pc_node* pcn) {
delete CONTAINER_OF(pcn, object, m_pcn);
}


static object* g_head = NULL;
static pc_master g_pcm = PC_MASTER_STATICINIT(&g_pcm, object_dtor);


void writer_push() {
object* const l_obj = new object;
object* l_cmp = g_head;
do {
l_obj->m_next = l_cmp;
} while (! CASPTR(&g_head, &l_cmp, l_obj));
}


void writer_pop() {
pc_region* const pcr = pc_acquire(&g_pcm);
object* l_cmp = g_head;
do {
if (! l_cmp) { break; }
} while (! CASPTR(&g_head, &l_cmp, l_cmp->m_next));
pc_release(&g_pcm, pcr);
if (l_cmp) {
pc_mutate(&g_pcm, &l_cmp->m_pcn);
}
}


void readers() {
pc_region* const pcr = pc_acquire(&g_pcm);
object* l_obj = g_head;
while (l_obj) {
l_obj->read_only_operation();
l_obj = l_obj->m_next;
}
pc_release(&g_pcm, pcr);
}
_____________________________________________________________________


This gives excellent performance because I can use raw pointers for the
readers and writers. This basically proves that reference counting can be
used is highly effective manner with hardly any overheads because of the
amortization that going on in the example above. This is just one example of
how flexible atomic reference counting can be...



A lot depends on how you use dynamic memory. The runtime of a
typical collector depends on the amount of memory actually in
use when the garbage collector runs; the runtime of a typical
manual memory allocator depends on the number of allocations and
frees. An application which uses a lot of small, short lived
objects, and disposes of adequate memory, will run significantly
faster with garbage collection. An application which only
allocates a few, very big object, will run faster with manual
collection or shared pointers.

What allocator algorithms are you talking about? There are allocators that
have virtually zero-overheads in that they have fast-paths which don't make
any use of atomic ops and/or memory barriers...
 
C

Chris Thomasson

Jon Harrop said:
Anything counting references.

non-sense. BTW, this seems to proves that you have little experience with
the more advanced forms of reference counting that have recently been
developed.

Memory barriers simply worsen the situation even more.

I said that there are counting algorithms that DO NOT use memory barriers
and/or atomic operations.


Okay, we are talking about the same thing in two different threads. Here is
the other post you made that is basically identical to this one:

http://groups.google.com/group/comp.programming.threads/msg/7c04692380331b62

and here is my reply:

http://groups.google.com/group/comp.programming.threads/msg/05a4e3e3a4a75f05

I think we should send follow-ups over there... Agreed?
 
J

Jon Harrop

Chris said:
non-sense. BTW, this seems to proves that you have little experience with
the more advanced forms of reference counting that have recently been
developed.

Where have you compared your invention to modern GCs?
I said that there are counting algorithms that DO NOT use memory barriers
and/or atomic operations.

Yes, I know. Reread what I wrote.
I think we should send follow-ups over there... Agreed?

Why there?
 
C

Chris Thomasson

[comp.programming.threads added]

Jon Harrop said:
Where have you compared your invention to modern GCs?

It's basically like comparing RCU to GC:

http://lwn.net/Articles/263130/#RCU is a Poor Man's Garbage Collector

It's like comparing apples to oranges because reference counting is not
really analogous to GC. For instance, a traditional reference count usually
destroys an object as soon as the last reference has been released;
basically it collects no garbage at all. This is not true for traditional
GC. BTW, the environments where vZOOM has been licensed to be used in could
not use a GC for various reasons.

Sometimes vZOOM is not the best candidate. If I am consulting for a job that
would greatly benefit from GC, I advise them to use it indeed. It's on a
case-by-case basis. GC is not a silver bullet. It never has been, and it
never will be. The same goes for vZOOM.



Yes, I know. Reread what I wrote.

You said that memory barriers make things worse; I agree. That's why I try
to eliminate them wherever I possibly can. Reference counts are a candidate
for this type of low-level optimization.



Why there?

I wanted to coalesce the two basically identical conversations into one.
 
C

Chris Thomasson

Jon Harrop said:
Alexander said:
Jon Harrop wrote:
[...]
I'm amazed anyone would bother putting a really poor GC in C++ rather
than

shared_ptr<> isn't really a GC. And if you view it from the GC angle, it
is indeed really poor.

What does it do apart from collect garbage?

shared_ptr<> destroys objects as soon as the reference count drops to one;
its basically 100% accurate. Traditional GC does not work that way at all.
 
C

Chris Thomasson

I have some code where objects are dynamically allocated by some
thread, and then used by multiple threads and freed when they are no
longer needed. I think that reference counting is the most appropriate
way to handle this in my situation.

However, my code currently has a very obvious problem in it. Here is
how I have reference counting implemented for my objects, basically
(sorry about any errors I am typing this here in this message):

=== BEGIN CODE ===

class A {
public:
A ();
void AddRef ();
void Release ();
private:
~A ();
int refs_; // reference count
pthread_mutex_t mtx_; // protects refs_
};

// constructor inits reference count to 1
A::A ()
: refs_(1), mtx_(PTHREAD_MUTEX_INITIALIZER)
{
}
[...]

You cannot use 'PTHREAD_MUTEX_INITIALIZER' that way. A:mtx_ is not an object
with static storage duration. Also, I don't think you can use
static-initalizers in a class constructor initalizers-list.
 
J

Jon Harrop

Chris said:
shared_ptr<> destroys objects as soon as the reference count drops to one;

That is garbage collection, IMHO.
its basically 100% accurate. Traditional GC does not work that way at all.

Indeed, traditional GCs can collect values sooner: before they go out of
scope.
 
C

Chris Thomasson

Jon Harrop said:
That is garbage collection, IMHO.


Indeed, traditional GCs can collect values sooner: before they go out of
scope.

Traditional GC does not perform a collection every single time an object
quiesces. If you are arguing that a "traditional" GC is more accurate than a
direct reference count, well, alls I can say is that your wrong on multiple
levels.
 
C

Chris Thomasson

Chris Thomasson said:
Traditional GC does not perform a collection every single time an object
quiesces. If you are arguing that a "traditional" GC is more accurate than
a direct reference count, well, alls I can say is that your wrong on
multiple levels.


BTW, have you ever considered the following:


ObjectA is being referenced in 3 places throughout a programA... Well, that
means that the number of references is 3. This holds true for traditional
GC, reference counting, or anything else that manages object lifetime.
Again, lets say there are 10 threads and 2 of them hold a single pointer to
an objectA... Well, guess what... The reference count of objectA is 2. I
could go on and on...

How is GC different from reference counting in that respect?

Please explain in detail...
 
C

Chris Thomasson

Well, they obviously leak if cycles are present. And while "an
order of magnitude slower" is obviously false as well, Hans
Boehm has performed a number of benchmarks, and they are slower
in a lot of cases.
A lot depends on how you use dynamic memory. The runtime of a
typical collector depends on the amount of memory actually in
use when the garbage collector runs; the runtime of a typical
manual memory allocator depends on the number of allocations and
frees. An application which uses a lot of small, short lived
objects, and disposes of adequate memory, will run significantly
faster with garbage collection. An application which only
allocates a few, very big object, will run faster with manual
collection or shared pointers.

What about an application that allocates a couple of bug buffers and carves
individual smaller objects out of them?
 
J

James Kanze

"James Kanze" <[email protected]> wrote in message
On 28 mar, 23:56, "Chris Thomasson" <[email protected]>
wrote:
GC does indeed detect when an object is not being referenced
by anything (e.g., in a quiescent state). IMHO, this is a
_very_ import part of lifetime management because you cannot
terminate an objects life if its being used by something else.

Garbage collection detects when an object can no longer be
referred to. Obviously, in this case, it's lifetime has ended,
since there is no longer any way to end it otherwise. But
that's about it---garbage collection will not collect the memory
underneath the object that is still alive. Provided, of course,
that there are not errors in the code, and you've lost the last
pointer to the object before its lifetime has ended. But that
has nothing to do with object lifetime per se.

Most objects have indeterminate lifetime, and it doesn't matter.
If an object has determinate lifetime, however, you have to
manage it, garbage collection or no. Garbage collection does
NOT manage object lifetime; it only manages memory.
 
J

Jon Harrop

Chris said:
BTW, have you ever considered the following:


ObjectA is being referenced in 3 places throughout a programA... Well,
that means that the number of references is 3. This holds true for
traditional GC, reference counting, or anything else that manages object
lifetime. Again, lets say there are 10 threads and 2 of them hold a single
pointer to an objectA... Well, guess what... The reference count of
objectA is 2. I could go on and on...

How is GC different from reference counting in that respect?

Please explain in detail...

It isn't. You are simply citing more irrelevant facts in an attempt to evade
my point.
 
D

David Schwartz

Think of the following scenario:
__________________________________________________
static atomic_ptr<foo> g_foo;

void writers() {
for(;;) {
local_ptr<foo> l_foo(new foo);
g_foo = l_foo;
}
}

g_foo = l_foo; should map to:

1) Lock g_foo.
2) If g_foo points to an object, release it.
3) Add a reference to l_foo.
4) Make g_foo point to l_foo.
5) Release the lock.
void readers() {
for(;;) {
local_ptr<foo> l_foo(g_foo);
if (l_foo) {
l_foo->do_something();
}
}}

Well, this can't be safe because there is no assurance l_foo will
continue to point to an object after the 'if' and until the call to
'do_something'. But if the intent was to make that safe, it would have
to be coded to make a copy of l_foo, then test what it points to, then
call a function on it.
Notice how the reader thread can grab a reference from 'g_foo' without
having a previous reference to an object contained within it? You can
download the sourcecode of atomic_ptr from:

Right, but threads aren't the only things that can have references.
Pointers and collections do too. In this case, the pointer has the
reference, which it gives a copy of to the thread. There is always
something that has a reference that gives you your reference.
Notice how it updates the counter and grabs a pointer to the current
region
in a single atomic operation (e.g., DWCAS-loop)? This is another solution
to
your puzzle.
You need to load a pointer and increment the reference count in a single
atomic operation.

No, you don't. Since you have a pointer, you have a reference. Since
you have a reference, there's no way the pointer can go away.

The solution is much simpler -- never a pointer without a reference.

This seems an awful lot of work to create a problem followed by an
awful lot of work to solve it. Just never allow a pointer to exist
without a reference. It seems illogical to allow this anyway, since
the pointer can't be guaranteed to point anywhere useful anyway.

If you get a pointer from somewhere, that somewhere must have the
pointer. Thus it must have a reference -- otherwise the pointer might
be invalid. Since the pointer is accompanied by a reference, there is
no risk that the pointer can become invalid until you get your
reference. All you have to do is sequence the operations that require
or involve the reference that accompanies that copy of the pointer.

DS
 
C

Chris Thomasson

David Schwartz said:
g_foo = l_foo; should map to:

1) Lock g_foo.
2) If g_foo points to an object, release it.
3) Add a reference to l_foo.
4) Make g_foo point to l_foo.
5) Release the lock.

It does not have to use locks. Have you even looked at the source code to
Joe Seighs atomic pointer?



Well, this can't be safe because there is no assurance l_foo will
continue to point to an object after the 'if' and until the call to
'do_something'. But if the intent was to make that safe, it would have
to be coded to make a copy of l_foo, then test what it points to, then
call a function on it.

Your incorrect here. This is 100% safe with strongly thread-safe reference
counting such as atomic_ptr.



Right, but threads aren't the only things that can have references.
Pointers and collections do too. In this case, the pointer has the
reference, which it gives a copy of to the thread. There is always
something that has a reference that gives you your reference.



No, you don't. Since you have a pointer, you have a reference. Since
you have a reference, there's no way the pointer can go away.

Did you even read the paper I cited?

http://citeseer.ist.psu.edu/cache/p...zSzpaperszSz01-podc.pdf/detlefs01lockfree.pdf



The solution is much simpler -- never a pointer without a reference.

This seems an awful lot of work to create a problem followed by an
awful lot of work to solve it. Just never allow a pointer to exist
without a reference. It seems illogical to allow this anyway, since
the pointer can't be guaranteed to point anywhere useful anyway.

If you get a pointer from somewhere, that somewhere must have the
pointer. Thus it must have a reference -- otherwise the pointer might
be invalid. Since the pointer is accompanied by a reference, there is
no risk that the pointer can become invalid until you get your
reference. All you have to do is sequence the operations that require
or involve the reference that accompanies that copy of the pointer.

Your thinking in terms of basic thread-safety which is indeed much simpler
to reason about. PLEASE read the cited paper, and then take a look at the
source-code to Joe Seighs excellent atomic_ptr:

http://sourceforge.net/project/showfiles.php?group_id=127837&package_id=139944&release_id=352395


Download it, and look in the 'i686' directory. Then open atomic_ptr.h.
Please!


BTW, did you read where the standard committee is thinking about making
shared_ptr honor strong thread-safety:

http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2007/n2297.html#atomic

?
 
C

Chris Thomasson

Jon Harrop said:
It isn't.

I agree that the essence of GC is not that different from reference counting
within the narrow context listed above.


You are simply citing more irrelevant facts in an attempt to evade
my point.

You point is that anything that uses any type of reference counting
whatsoever is slow, outdated, crappy... Either your a troll, or your a GC
religious freak. I never said that GC should never be used. I said I pick
the right tool for the job at hand.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,755
Messages
2,569,535
Members
45,007
Latest member
obedient dusk

Latest Threads

Top