
Reclaiming C++ Data Structures with Cycles - criddell
https://www.justsoftwaresolutions.co.uk/cplusplus/reclaiming-data-structures-with-cycles.html
======
ingenter
I recently watched Herb Sutter's talk "Leak-Freedom in C++", described in the
article [1], and I can't help but notice that C++ community has a strong bias
against garbage collection. ... And then he describes the very problem you
can't solve without GC: memory management for graphs with cycles. Solving this
problem is equivalent to implementing GC. Of course, having your own
specialized GC may help, but you may also benefit from your own memory
allocation.

Why can't you acknowledge that there are problems that have GC as _the only
and best_ solution?

(Note: it's possible to mix memory management approaches, e.g only pointers to
graph nodes are being garbage collected and everything else has a nice
hierarchical ownership.)

[1]
[https://www.youtube.com/watch?v=JfmTagWcqoE](https://www.youtube.com/watch?v=JfmTagWcqoE)

~~~
jasode
_> I can't help but notice that C++ community has a strong bias against
garbage collection._ [...]

 _> Why can't you acknowledge that there are problems that have GC as the only
and best solution?_

Your prelude and the followup question is not well-formed.

C++ programmers do not have a bias against GC as a _specific_ problem-solving
technique. In fact, expert C++ programmers can embrace GC so much that they
can write an entire virtual machine[1] with GC and a DSL[2] for that vm that
takes advantage of memory safety. Both the CLR vm and the (original) C#
compiler were written by C++ programmers.

What the C++ community doesn't want is GC in the C++ base language itself or
the standard runtime. That's a very different concept from a _generalized_
"C++ bias against GC".

In other words, the following is unacceptable:

    
    
      std::string x = "Hello, " + fullname; // cpu cycles spent on GC
    

Those cpu cycles spent on constantly checking if "x" is no longer reachable is
cpu power that's taken away from rendering frames of a 60fps game, or
computing numeric equations or high speed quantitative trading. C++
programmers don't want GC as a global runtime that you can't opt out of. Also,
global GC often requires 2x-3x the memory footprint of working memory which is
extremely wasteful for the resource constrained domains that C++ is often used
in.

Herb Sutter's presentation is compatible with "pseudo-GC-when-you-need-it"
without adding GC to the entire C++ standard runtime.

[1][https://en.wikipedia.org/wiki/Common_Language_Runtime](https://en.wikipedia.org/wiki/Common_Language_Runtime)

[2][https://en.wikipedia.org/wiki/C_Sharp_(programming_language)](https://en.wikipedia.org/wiki/C_Sharp_\(programming_language\))

~~~
chrisseaton
I agree with you, but in this

> std::string x = "Hello, " \+ fullname; // cpu cycles spent on GC

you are already spending cycles on memory management (if C++ allocates
character data on the heap which I think it does). You are searching for free
space in the heap, possibly synchronising to do that, and so on.

With a GC you may even use less cycles here! For example a copying GC could
mean that you can allocate with a simple thread local bump pointer.

So in your statement you are already paying an unknown cycle cost for memory
management. Why do you care if it's GC?

Your answer is probably the variance in the number of cycles - the noticeable
pauses - which is a reasonable concern.

~~~
daemin
Yes, but in this case we know when the allocations will occur, and when they
will be freed. If using a GC we know when they will occur, but do not know
when they will be freed. Which means that at some indeterminate point in the
future there will be a large temporary slowdown due to processing the GC.

This is one of the bigger reasons people use C++ and even techniques within it
to explicitly collect such items at a known point in time. (Techniques such as
marking items as dead in an array but still keeping them in there until the
end of frame, etc)

~~~
titzer
> If using a GC we know when they will occur, but do not know when they will
> be freed. Which means that at some indeterminate point in the future there
> will be a large temporary slowdown due to processing the GC.

This just _isn 't true anymore_. Incremental collectors can achieve pause
times in the single-digit millisecond range, and concurrent collectors can
achieve pause times in the single-digit to tens of microseconds range, even
for super-high-allocation rate programs. There are even real-time collectors
suitable for audio and other hard real-time applications.

Azul GC (a high performance concurrent compacting collector):
[https://www.azul.com/products/zing/pgc/](https://www.azul.com/products/zing/pgc/)

Metronome (a real-time GC with guaranteed maximum pause times):
[http://researcher.watson.ibm.com/researcher/view_group_subpa...](http://researcher.watson.ibm.com/researcher/view_group_subpage.php?id=175)

GC scheduling in V8 (hides GC pause times between frames, reducing animation
jank):
[http://queue.acm.org/detail.cfm?id=2977741](http://queue.acm.org/detail.cfm?id=2977741)

The Go GC ships now with a very low-latency GC:
[https://blog.golang.org/go15gc](https://blog.golang.org/go15gc)

~~~
paulsutter
> This just isn't true anymore. Incremental collectors can achieve pause times
> in the single-digit millisecond range

Single digit milliseconds is millions of instructions, which /is/ a large
slowdown in some applications.

~~~
titzer
How much do you think a page fault costs?

~~~
paulsutter
Page faults cost zero. On locked pages.

------
putzdown
This is a very promising solution. If it has a fatal problem (I'm not sure it
does), it would be the cost of searching back through the referrer chain to
look for a root pointer. Am I wrong in thinking that that will be at least
O(n), if not O(n^2)? And n could be extremely large—as large as the number of
objects in your program.

To say it another way: what we (C++ programmers, stereotypically) don't like
about GC is that it incurs long search costs at unpredictable intervals. That
long search cost is not dissimilar in nature (i.e. in algorithm) to the cost
of this search. So it's not clear to me that the performance characteristics
of `internal_ptr` would be much different from those of GC. I'd like to see
the performance characteristics of the solution filled out with more detail.

------
iainmerrick
The author's "internal_ptr" template seems exactly equivalent to Objective-C
(and I assume Swift)'s weak references.

And that's great! Automatic reference counting is my single favourite thing
about Obj-C. Once you get the hang of using weak refs for parent objects, ARC
is very easy to use, with _consistent_ speed, low memory usage, and immediate
deallocation of dead objects.

I'd go as far as to say that Objective-C is a big reason why iOS has
historically been faster and smoother than Android. It's not the world's
fastest language, but there are no big GC pauses and it doesn't waste a lot of
memory.

So I bet this is a good memory management style for C++ too.

~~~
putzdown
No, Obj-C/Swift use something more akin to C++'s `shared_ptr` and `weak_ptr`,
as evidenced by the fact that cyclic references may result in memory leaks in
each system. This is precisely the problem that `internal_ptr` seeks to
resolve.

~~~
iainmerrick
Ahh, rereading more carefully, I think I see. You could have a chain of
objects connected by internal pointers, and they'd all be kept alive as long
as the first object is strongly referenced, but that wouldn't work for weak
pointers.

------
72deluxe
Really fascinating article. I'm yet to look at the library and Herb's library
too but great to have both.

The cppcon YouTube channel is full of videos that will take up my evenings, to
my wife's disgust (doubt she wants to watch Bjarne talk).

------
radarsat1
Such effort would be better spent on figuring out best practices for avoiding
data structures with cycles in the first place.

~~~
putzdown
I think you've misunderstood the article. It's talking about the object
dependency (pointer) graph, not data structures in a more general sense. The
object dependency graph is intrinsically cyclical in the way the OP describes.
There's no avoiding it.

~~~
radarsat1
No need to be condescending.

The article talks duly about the data structure in question and is perfectly
interesting. What it doesn't address is when this data structure comes up in
practice and whether it can be handled in different ways.

In many cases, it might be desirable to search for alternative memory
representations, since this data structure obviously complicates things
considerably (for C++).

For example, a graph is isomorphic to a sparse array, which could avoid
embedding bidirectional pointers directly into the graph nodes.

