40

I really like scope-based memory management (SBMM), or RAII, as it is more commonly (confusingly?) referred to by the C++ community. As far as I know, except for C++ (and C), there's no other mainstream language in use today that makes SBMM/RAII their main memory management mechanism, and instead they prefer to use garbage collection (GC).

I find this rather confusing, since

  1. SBMM makes programs more deterministic (you can tell exactly when an object is destroyed);
  2. in languages that use GC you often have to do manual resource management (see closing files in Java, for example), which partly defeats the purpose of GC and is also error prone;
  3. heap memory can also (very elegantly, imo) be scope-bound (see std::shared_ptr in C++).

Why is not SBMM more widely used? What are its disadvantages?

8
  • 1
    Some disadvantages (especially concerning speed) are discussed in Wikipedia: en.wikipedia.org/wiki/…
    – Philipp
    Mar 9, 2014 at 14:29
  • 2
    Java's manual resource management problem is a side effect of not guaranteeing that an object's finalize() method will be called prior to garbage collection. In effect, this creates the same class of problem that garbage collection is supposed to solve.
    – Blrfl
    Mar 9, 2014 at 15:00
  • 7
    @Blrfl Nonsense. "Manual" resource management (for resources other than memory, obviously) would be preferable even if that "problem" did not exist, because GC can run a very long time after the resource becomes unused, or even not run at all. That's no problem for memory, and memory management is all that garbage collection is supposed to solve.
    – user7043
    Mar 9, 2014 at 15:23
  • 4
    btw. I like to refer to it as SBRM since you can use the same mechanism to manage resources in general, not just memory.
    – PlasmaHH
    Mar 10, 2014 at 10:48
  • 2
    related: Did the developers of Java consciously abandon RAII?
    – gnat
    May 21, 2014 at 21:21

8 Answers 8

28

Let's start by postulating that memory is by far (dozens, hundreds or even thousands of time) more common than all other resources combined. Every single variable, object, object member needs some memory allocated to it and freed later on. For every file you open, you create dozens to millions of objects to store the data pulled out of the file. Every TCP stream goes together with an unbounded number of temporary byte strings created to be written to the stream. Are we on the same page here? Great.

For RAII to work (even if you have ready-made smart pointers for every use case under the sun), you need to get ownership right. You need to analyse who should own this or that object, who should not, and when ownership should be transferred from A to B. Sure, you could use shared ownership for everything, but then you'd be emulating a GC via smart pointers. At that point it becomes much easier and faster to build the GC into the language.

Garbage collection frees you from this concern for the by far most commonly used resource, memory. Sure, you still need to make the same decision for other resources, but those are far less common (see above), and complicated (e.g. shared) ownership is less common too. The mental burden is reduced significantly.

Now, you name some downsides to making all values garbage collected. However, integrating both memory-safe GC and value types with RAII into one language is extremely hard, so perhaps it's better to migitate these trade offs via other means?

The loss of determinism in turns out to be not that bad in practice, because it only affects deterministic object lifetime. As described in the next paragraph, most resources (aside from memory, which is plentiful and can be recycled rather lazily) are not bound to object lifetime in these languages. There are a few other uses cases, but they are rare in my experience.

Your second point, manual resource management, is nowadays addressed via a statement that does perform scope-based cleanup, but does not couple this clean up to the object life time (hence not interacting with the GC and memory safety). This is using in C#, with in Python, try-with-resources in recent Java versions.

16
  • 8
    @Philipp Shared ownership for everything is a GC, just a very poor one. If you take "shared ownership" to imply ref counting, please say so, but keep the discussion about cycles in the comments on amon's answer. I'm also not sure whether ref counting is deterministic in the sense OP is interested in (objects are freed as early as possible, discounting cycles, but you often can't tell when that is by looking at the program). Moreover, ref counting for everything is slow, much slower than a modern tracing GC.
    – user7043
    Mar 9, 2014 at 14:42
  • 16
    using is a joke compared to RAII, just so you know.
    – DeadMG
    Mar 10, 2014 at 0:10
  • 3
    @Philipp please describe your metric for "superior". It is true that manual memory management is faster at run-time for dealing with memory management. However the cost of software cannot be judged purely on the CPU time spent on memory management alone.
    – ArTs
    Mar 10, 2014 at 7:29
  • 3
    @ArTs: I wouldn't even necessarily agree with this. RAII comes with the requirement that an object must be destroyed as it leaves scope. A loop, then, is required to do n object destructions. Under a modern generational GC, those destructions could be deferred until the end of the loop, or even later, and perform just one operation to destroy hundreds of iterations worth of memory. A GC can be very very fast in its good cases.
    – Phoshi
    Mar 10, 2014 at 9:39
  • 2
    Not really convinced. Most of the described uses of memory are essentially values, where you don't care about ownership. Here I have a string, I pass the thing, just as easy as an integer. The string doesn't have complex ownership internals (typically one private buffer, unshared) and in turn the string owner probably owns it outright (on stack, member, unique_ptr)
    – MSalters
    Mar 10, 2014 at 12:45
15

RAII also follows from automatic reference-counting memory management, e.g. as used by Perl. While reference counting is easy to implement, deterministic, and quite performant, it cannot deal with circular references (they cause a leak) which is why it isn't commonly used.

Garbage-collected languages can't use RAII directly, but often do offer syntax with an equivalent effect. In Java, we have the try-with-ressource statement

try (BufferedReader br = new BufferedReader(new FileReader(path))) { ... }

which automatically calls .close() on the resource on block exit. C# has the IDisposable interface, which allows .Dispose() to be called when leaving an using (...) { ... } statement. Python has the with statement:

with open(filename) as f:
    ...

which works in a similar fashion. In an interesting spin on this, Ruby's file open method gets a callback. After the callback has been executed, the file is closed.

File.open(name, mode) do |f|
    ...
end

I think Node.js uses the same strategy.

7
  • 4
    Using higher order functions for resource management dates back long before Ruby. In Lisps, it is quite common to have, say, with-open-filehandle functions which open the file, yield it to a function and upon return of the function close the file again. Mar 9, 2014 at 14:15
  • 4
    The cyclic reference argument is quite common, but how important is it really? Cyclic references can be mitigated using weak pointers if ownership is clear.
    – Philipp
    Mar 9, 2014 at 14:23
  • 2
    @Philipp When you use ref counting, ownership usually isn't clear. Also, this answer talks about languages which use ref counting exclusively and automatically, so weak references either don't exist or are much harder to use than strong references.
    – user7043
    Mar 9, 2014 at 14:28
  • 3
    @Philipp cyclic data structures are very rare, unless you're working with complicated graphs anyway. Weak pointers do not help in a general cyclic object graph, although they help in more common cases like parent pointers in a tree. A good workaround is to keep a context object that represents the reference to the whole graph, and manages destruction. Refcounting is not a dealbraker, but it requires that the programmer is very aware of its restrictions. I.e. it has a slightly higher cognitive cost than GC.
    – amon
    Mar 9, 2014 at 14:28
  • 1
    An important reason why ref counting is rarely used is that it's often slower than GC, despite its simplicity.
    – Rufflewind
    Mar 15, 2014 at 6:34
13

In my opinion the most convincing advantage of garbage collection is that it allows for composability. Correctness of memory management is a local property in garbage collected environment. You can look at each part in isolation and determine if it can leak memory. Combine any number of memory-correct parts and they stay correct.

When you rely on reference counting you lose that property. Whether your application can leak memory becomes a global property of the whole application with reference counting. Every new interaction between parts has the possibility to use the wrong ownership and break memory management.

It has a very visible effect on the design of programs in the different languages. Programs in GC-languages tend to be a bit more soups of objects with lots of interactions, while in GC-less languages one tends to prefer structured parts with strictly controlled and limited interactions between them.

7
  • 1
    Correctness is only composable if objects only hold references on their own behalf, and not on behalf of the targets of those references. Once things like notifications enter into the mix (Bob holds a reference to Joe because Joe asked Bob to notify him when something happened and Bob promised to do so, but Bob otherwise doesn't care about Joe), GC correctness often requires scoped resource management [manually implemented in many cases, since GC systems lack the automation of C++].
    – supercat
    Mar 18, 2014 at 23:46
  • @supercat: "GC correctness often requires scoped resource management". Eh? Scope only exists in the source code and GC only exists at run-time (and is, therefore, completely oblivious to the existence of scope).
    – J D
    Jan 13, 2015 at 6:40
  • @JonHarrop: I was using the term "scope" in the same sense as a C++ "scoped pointer" [the lifetime of the object should be that of the container holding it], since that's the usage implied by the original question. My point is that objects create potentially-long-lived references to themselves for purposes such as receiving events may not be composable in a purely-GC system. For correctness, certain references need to be strong and certain references need to be weak, and which references need to be which will depend upon how an object is used. For example...
    – supercat
    Jan 13, 2015 at 16:07
  • ...suppose objects Fred and Barney sign up for notification whenever anything in a certain directory is modified. Fred's handler does nothing but increment a counter whose value it can report on request, but for which it has no other use. Barney's handler will pop up a new window if a certain file is modified. For correctness, Fred should be subscribed with a weak event, but Barney's should be strong, but the timer object will have no way of knowing that.
    – supercat
    Jan 13, 2015 at 16:11
  • @supercat: Right. I wouldn't say that happens "often". I've only come across it once in 30 years of programming.
    – J D
    Jan 13, 2015 at 20:31
7

Closures are an essential feature of pretty much all modern languages. They are very easy to implement with GC and very hard (though not impossible) to get right with RAII, since one of their main features is that they allow you to abstract over the lifetime of your variables!

C++ only got them 40 years after everybody else did, and it took a lot of hard work by a lot of smart people to get them right. In contrast, many scripting languages designed and implemented by people with zero knowledge in designing and implementing programming languages have them.

4
  • 9
    I don't think closures in C++ are a good example. The lambdas in C++11 are just syntactic sugar for functor classes (which pre-date C++11 significantly), and are equally memory-unsafe: If you capture something by reference, and call the closure after that thing goes dead, you simply get UB, much like holding onto a reference longer than valid. That they appeared 40 years late is due to belated acknowledgement of FP, not due to figuring out how to make them safe. And while designing them was certainly a huge task, I doubt that most of the effort went into life time considerations.
    – user7043
    Mar 9, 2014 at 14:22
  • I agree with delnan: C++ did not get closures right: you have to program them very carefully if you do not want to get a core dump when you invoke them.
    – Giorgio
    Mar 9, 2014 at 15:49
  • 2
    @delnan: capture-by-reference lambda's very intentionally have that [&] syntax. Any C++ programmer already associates the & sign with references and knows about stale references.
    – MSalters
    Mar 10, 2014 at 12:50
  • 2
    @MSalters What's your point? I've drawn the reference connection myself. I did not say C++ lambdas are exceptionally unsafe, I said they are exactly as unsafe as references. I did not argue that C++ lambdas are bad, I argued against this answer's claim (that C++ got closures very late because they had to figure out how to do it right).
    – user7043
    Mar 10, 2014 at 13:02
5
  1. SBMM makes programs more deterministic (you can tell exactly when an object is destroyed);

For most programmers the OS is non-deterministic, their memory allocator is non-deterministic and most of the programs they write are concurrent and, therefore, inherently non-deterministic. Adding the constraint that a destructor is called exactly at the end of scope rather than slightly before or slightly after is not a significant practical benefit for the vast majority of programmers.

  1. in languages that use GC you often have to do manual resource management (see closing files in Java, for example), which partly defeats the purpose of GC and is also error prone;

See using in C# and use in F#.

  1. heap memory can also (very elegantly, imo) be scope-bound (see std::shared_ptr in C++).

In other words, you could take the heap which is a general purpose solution and change it to only work in a specific case that is seriously limiting. That is true, of course, but useless.

Why is not SBMM more widely used? What are its disadvantages?

SBMM limits what you can do:

  1. SBMM creates the upward funarg problem with first-class lexical closures which is why closures are popular and easy to use in languages like C# but rare and tricky in C++. Note that there is a general trend towards the use of functional constructs in programming.

  2. SBMM requires destructors and they impede tail calls by adding more work to do before a function can return. Tail calls are useful for extensible state machines and are provided by things like .NET.

  3. Some data structures and algorithms are notoriously difficult to implement using SBMM. Basically anywhere that cycles are naturally occurring. Most notably graph algorithms. You effectively end up writing your own GC.

  4. Concurrent programming is harder because control flow and, therefore, object lifetimes are inherently non-deterministic here. Practical solutions in message passing systems tend to be deep copying of messages and the use of excessively long lifetimes.

  5. SBMM keeps objects alive until the end of their scope in the source code which is often longer than necessary and can be far longer than necessary. This increases the amount of floating garbage (unreachable objects waiting to be recycled). In contrast, tracing garbage collection tends to free objects soon after the last reference to them disappears which can be much sooner. See Memory management myths: promptness.

SBMM is so limiting that programmers need an escape route for situations where lifetimes can not be made to nest. In C++, shared_ptr offers an escape route but it can be ~10x slower than tracing garbage collection. So using SBMM instead of GC would put most people wrong footed most of the time. That is not to say, however, that it is useless. SBMM is still of value in the context of systems and embedded programming where resources are limited.

FWIW you might like to check out Forth and Ada, and read up on the work of Nicolas Wirth.

10
  • 1
    If you say which bits I may be able to elaborate or cite articles.
    – J D
    Jan 13, 2015 at 19:21
  • 2
    How relevant is being 10x slower in a few rare use cases as opposed to being omnipresent in all use cases? C++ has unique_ptr and for most purposes its sufficient. Next to that, rather than attacking RAII trough C++ (a language that many love to hate for being an archaic language), if you are going to attack RAII trough attacking a language, try a younger sibling of the RAII family, Rust for example. Rust basically gets everything right that C++ got wrong while it gets most things right that C++ got right also. Further 'using' gets you a very limited set of use cases and ignores composition. Jan 14, 2015 at 14:54
  • 2
    "How relevant is being 10x slower in a few rare use cases as opposed to being omnipresent in all use cases?". Firstly, that's a circular argument: shared_ptr is only rare in C++ because it is so slow. Secondly, that is an apples and oranges comparison (as the article I cited already showed) because shared_ptr is many times slower than a production GC. Thirdly, GCs are not omnipresent and are avoided in software like LMax and Rapid Addition's FIX engine.
    – J D
    Jan 15, 2015 at 20:42
  • 1
    @Jon Harrop, If you havn't please enlighten me. What magic recipe have you used trough that 30+ years to mitigate the transitive effects of using deep resources? Without such a magic recipe after 30+ years I could only conclude that you must have misattributed being bitten by it to other causes. Jan 21, 2015 at 9:06
  • 1
    @Jon Harrop, shared_ptr isn't rare because its slow, its rare for the reason that in a decently designed system the need for 'shared ownership' is rare. Jan 21, 2015 at 9:12
4

Looking at some popularity index like TIOBE (which is arguable, of course, but I guess for your kind of question its ok to use this) , you first see that ~50% of the top 20 are "scripting languages" or "SQL dialects", where the "ease of use" and means of abstraction have a much more importance than deterministic behaviour. From the remaining "compiled" languages, there are around 50% of the languages with SBMM and ~50% without. So when taking the scripting languages out of your calculation, I would say your assumption is just wrong, among compiled languages the ones with SBMM are as popular as the ones without.

4
  • 1
    How is "ease of use" different from determinism? Shouldn't a deterministic language be considered easier to use than a non-deterministic one?
    – Philipp
    Mar 9, 2014 at 14:21
  • 2
    @Philipp Only of the thing that's deterministic or not actually matters. Object life time doesn't matter on its own (though C++ and friends tie many things which matter to object life time, because they can). When an unreachable object is freed doesn't matter because, by definition, you aren't using it any more.
    – user7043
    Mar 9, 2014 at 14:25
  • One more thing, various "scripting languages" like Perl and Python also use reference counting as primary means for memory management.
    – Philipp
    Mar 10, 2014 at 12:57
  • 1
    @Philipp At least in the Python world, this is considered an implementation detail of CPython, not a property of the language (and virtually every other implementation eschews refcounting). Moreover, I would argue that catch-all no-opt-out reference counting with backup cycle GC does not qualify as SBMM or RAII. In fact, you'd be hard pressed to find RAII proponents that are consider this style of memory management comparable to RAII (mostly because it isn't, cycles anywhere can prevent prompt deallocation anywhere else in the program).
    – user7043
    Mar 10, 2014 at 14:26
3

One major advantage of a GC system which nobody has mentioned yet is that a reference in a GC system is guaranteed to retain its identity as long as it exists. If one calls IDisposable.Dispose (.NET) or AutoCloseable.Close (Java) on an object while copies of the reference exist, those copies will continue to refer to the same object. The object won't be useful for anything anymore, but attempts to use it will have predictable behavior controlled by the object itself. By contrast, in C++, if code calls delete on an object and later tries to use it, the entire state of the system becomes totally undefined.

Another important thing to note is that scope-based memory management works very well for objects with clearly-defined ownership. It works much less well, and sometimes downright badly, with objects that have no defined ownership. In general, mutable objects should have owners, while immutable objects don't need to, but there's a wrinkle: it's very common for code to use an instance of a mutable types to hold immutable data, by ensuring that no reference will be exposed to code that might mutate the instance. In such a scenario, instances of the mutable class might be shared among multiple immutable objects, and thus have no clear ownership.

8
  • 4
    The property you refer to in the first half is memory safety. While a GC is a very easy way to achieve memory safety, a GC is not necessary: Look at Rust for a well-done example.
    – user7043
    Mar 10, 2014 at 0:37
  • @delnan: When I view rust-lang.org, my browser can't seem to navigate usefully from there; where should I look for more information? My impression is that memory safety without GC imposes certain restrictions on data structures which may not fit well with all the things an application might need to do, but I'd be happy to be proven wrong.
    – supercat
    Mar 10, 2014 at 2:53
  • 1
    I don't know of a single (or even small set of) good references for this; my Rust knowledge has accumulated over a year or two of reading the mailing list (and all the stuff linked to in mails, including various tutorials, language design blog posts, github issues, ThisWeekInRust, and more). To briefly address your impression: Yes, each safe construct (necessarily) impose restrictions, but for virtually any memory safe piece of code, an appropriate safe construct exists or can be written. The by far most common ones already exist in language and stdlib, all others can be written in user code.
    – user7043
    Mar 10, 2014 at 12:26
  • @delnan: Does Rust require interlocked updates to reference counters, or does it have some other means of dealing with immutable objects (or immutably-wrapped mutable objects) which have no definite ownership? Does Rust have a concept of both "object-owning" and "non-owning" pointers? I recall a paper on "Xonor" pointers which discussed the idea of objects having a single reference that "owns" them, and other references which do not; when the "owning" reference goes out of scope, all non-owning references would become references to dead objects, and would be identifiable as such...
    – supercat
    Mar 10, 2014 at 16:24
  • 1
    I don't think Stack Exchange comments are the right medium to give a tour through a language. If you're still interested, you can go straight to the source (#rust IRC, rust-dev mailing list, etc.) and/or hit me up with a chat room (you should be able to create one).
    – user7043
    Mar 11, 2014 at 21:16
-2

First off, its very important to realize that equating RAII to SBMM. or even to SBRM. One of the most essential (and least known or most under appreciated) qualities of RAII is the fact that it makes 'being a resource' a property that is NOT transitive to composition.

The following blog post discusses this important aspect of RAII and contrasts it to resource amangement in GCed languages that use non-deterministic GC.

http://minorfs.wordpress.com/2011/04/29/why-garbage-collection-is-anti-productive/

Its important to note that while RAII is mostly used in C++, Python (at last the non-VM based version) has destructors and deterministic GC that allows RAII to be used together with GC. Best of both worlds if it were.

16
  • 1
    -1 That's one of the worst articles I've ever read.
    – J D
    Jan 13, 2015 at 6:42
  • 1
    The problem in languages isn't that they support GC, but that they abandon RAII. There's no reason a language/framework shouldn't be able to support both.
    – supercat
    Jan 13, 2015 at 17:23
  • 1
    @Jon Harrop, could you elaborate. Of the claims made in the article, is there even one of the first 3 claims that doesn't hold? I think you might disagree on the productivity claim, but the other 3 claims are absolutely valid. Most importantly the first one about the transitivity of being a resource. Jan 14, 2015 at 14:30
  • 2
    @user1703394: Firstly, the entire article is based around a strawman "GCed language" when, in fact, it has nothing whatsoever to do with garbage collection. Secondly, he blames garbage collection when, in fact, the faults lie with object oriented programming. Finally, his argument is 10 years too late. The vast majority of programmers already modernised to garbage collected languages precisely because they offer much higher productivity.
    – J D
    Jan 15, 2015 at 0:31
  • 1
    His concrete examples (RAM, open file handles, locks, threads) are quite telling. I'm hard pressed to recall the last time I had to write code that dealt directly with any of those. With RAM, the GC automates everything. With file handles I write code like File.ReadLines file |> Seq.length where the abstractions handle closing for me. Locks and threads I've replaced with .NET Task and F# MailboxProcessor. This whole "We exploded the amount of manual resource management" is just complete nonsense.
    – J D
    Jan 15, 2015 at 0:36

Not the answer you're looking for? Browse other questions tagged or ask your own question.