Removing garbage collection from the Rust language
pcwalton.github.io
pcwalton.github.io
But it's important to point out the general importance of good garbage collectors. GCs are importnat not only because they help avoid a common type of bugs, but because they are essential to many concurrent data structures. Performance of modern software is now (and more so in the future) largely determined not by the single-thread speed of running an algorithm, but the scalability of an algorithm as it parallelizes across cores. Many of the most scalable concurrent data structures are almost impossible to implement robustly without a really good garbage collector.
As of now, the only really-good GCs are found in the JVM, and Java does indeed provide very low-level access to important memory primitives (direct control of memory fences is now possible through an undocumented API, and will be made public probably in the forthcoming Java 8). But as good and as powerful the JVM is, such algorithms will be necessary in circumstances where the JVM is not the best choice, namely embedded systems. So a garbage collector as good as those available for the JVM (or almost as good) but for a low-level systems language will be a boon.
A good GC for C++ would probably be a headache because of the language's inherent unsafety in just about anything.
Go is not an option in those circumstances, either. For one, while Go compiles to native binaries, the language actually operates at a higher level than Java (for example, it offers no control over thread scheduling like Java does). More importantly, choices made by the language designers might preclude a GC implementation good enough for high-performance concurrent data structures.
Rust is a great language to try and build a good GC for. If it would be possible to do so in a library and not in the core language - all the better.
If you go into any Java shop (which is where I live) and try to run a highly available system with more than 8GB of RAM in a server instance, you will be lucky to do that.
Servers on the cheap exist with 256GB of RAM, if you can't get to 12GB without OOMs, and GC pauses of 30 seconds, you will never get to a process that scales to this hardware. Your GC pause on a 100GB system would be over a minute complete stop the world probably more.
And that's a shame, because the only antidote, is to over-distribute, and over cluster, and not only slow everything down by 1000X compared to a single server, but the engineering and operational costs to distribute everything into tiny 4GB buckets are also unacceptable.
I think the industry should rethink GC, if it can get past this boundary. 4GB walls and 8GB walls compared to the current availability of hardware is embarrassing.
I would rather not build a server that can't use all the memory I want it to without falling over.
My minimum required RAM metric is 256GB for a process. That could even be a fairly small (code wise) system that just needs a lot of RAM in which case manual memory management is a piece of cake.
It's a set of patches to Linux, to cheaply implement the memory management tricks, e.g. batched operations on big pages without redundant TLB clearing, and a set of patches to the Hotspot JVM, which is where I think most people had trouble digesting it. See e.g. http://en.wikipedia.org/wiki/User:AzulPM/Managed_Runtime_Ini... for more details.
It ideally would have gotten enough traction that the Linux patches would get accepted in the mainline kernel. Ah well, perhaps someday.
We aren't running machines with 100 GB heaps, but i can tell you from personal experience that it's possible to exceed 8 GB without difficulty.
I doubt you'd have to look far to find people who were running with substantially bigger heaps on stock Sunacle/OpenJDK JVMs.
Also remember that if you are on linux doing that 28 GB heap you are living in the world of memory over-commit, which means if you actually tried to use that memory, you will probably segfault.
Again if you have done it great, no shop I have worked in, has been able to do it with app servers and keep their servers up reliably. Its a constant source of pain and I think JVM GC is not sufficient to scale at the level of current hardware.
Circular references are a big problem, because even if you have a single circular reference, a significant portion of your object graph may leak. (Say A -> B -> C -> D -> E -> ..., and also B -> A. A tiny problem like this has just caused your entire object graph to leak).
I think such structures are useful (and nice!) in some circumstances, but as a general solution for efficient system software they're just too hungry to be useful, at least for now. Having useful memory cut to 1/6th (if I'm being generous) or lower is painful, and god forbid the operating system decides it's time to start swapping out pages. The effective memory bandwidth will take a kick in the pants too.
No silver bullet, &c.
But even mutable concurrent data structures need to keep the old version around, albeit momentarily (hopefully), as they CAS in a new node (depending on the nature of the data structure) in the presence of concurrent readers. Achieving even that with simple ref-counting is not trivial.
Interesting. Rust currently has an atomically-reference-counted type (ARC, briefly mentioned in the OP's post) for sharing data between tasks. I wonder if the algorithm they describe would be useful for Rust as well.
OpenSceneGraph [1] uses intrusive reference counting pointers yet has been found suitable for many real time applications.
Nothing about modern hardware is the same as it used to be. I strongly recommend watching this extremely enlightening talk by Doug Lea about designing multi-threaded algorithms (http://emergingtech.chariotsolutions.com/2013/04/phillyete-s...). For example, did you know that in order for a busy-wait (spin loop) to be effective on modern intel CPUs, it has to compute random numbers (otherwise, the CPU might think you're in the OS idle-loop and power-down the core)?
Regarding CAS on every pointer copy, it's true that the current hardware has problem with it. Still I wonder if it would be worth and enough to optimize some future CPU to speed up the "interlocked increments" and "interlocked decrements" by introducing delays only to the cores that "interockedly" access the same addresses? I guess it can be tricky as one core has a lot of "in-flight" memory operations at the same tick, but I don't know the real limitations or if there's something more I'm missing, anybody reading this who knows more, what are the limitations?
Thread-safe deferred reference counting looks like GC.
Is there any evidence that this is true for most modern software? I can think of far more instances of software that use threads for simple things like networking or UI because it is easier and/or their library makes them than software that is so CPU-bound for single algorithms that they need to parallelize to maximize performance across CPUs.
Now you could argue whatever shared data store they're accessing needs to run things concurrently in order to maximize performance (and certainly many do), but even then we're usually looking at relatively simple shared memory patterns often with fairly coarse locking that scales well to a few cores, but not much beyond that (which often doesn't matter since you can saturate your network and/or memory bus before that). Maybe once in a while you'll see a lock-less concurrent data structure, but they're still fairly exotic.
What? Care to elaborate? (AFAIK, the only popular single-threaded webapp server is node.js, which is one of the slower web servers around: http://www.techempower.com/benchmarks/#section=data-r4)
...that scales well to a few cores, but not much beyond that (which often doesn't matter since you can saturate your network and/or memory bus before that)
You're describing limitations to current implementations that seriously hinder scaling. Developers are far from happy with this state of affairs, which, thankfully is not a necessity. You most certainly will not saturate the network if the data is distributed wisely. Here's a post I once wrote for highscalability.com that discusses the networking issue: http://highscalability.com/blog/2012/8/20/the-performance-of...
The memory bus is a bigger problem in general: if you do enough work on enough cores you will saturate the bus. But this, too, is simply the current state of affairs and not some insurmountable barrier to scaling.
To see how hard the implementation of a parallel and concurrent garbage collector can be, take a look at the Garbage-First Garbage Collection paper. Try to really understand all the moving parts in there and how they interact. Once you're done wiping your brains off the wall, realize that this is the extent that GC engineers need to go to remove most bottlenecks on scaling, regardless of your opinion of G1 itself. This is a serious and hard-core engineering effort that absorbs several man years of expert effort (and I emphasize the expert part) to do properly. Rust would probably have to become the size and importance of the Java world to ever get this level of attention. Despite this, the JVM's collectors still routinely throw roadblocks in front of the mutator.
And that ignores the other tradeoffs that GCs present. E.g. most GCs need at least 3x working set to work reasonably efficiently. That's 3x less working set you could be keeping in a local memory bank, or using for something else (like a disk cache, because page faults sure are crazy expensive...).
The best way to avoid this problem is to not play the game at all. The reason I became interested in Rust in the first place is Graydon and co wisely did not follow that pied piper. Optional thread-local garbage collectors are a much simpler problem.
As an unrelated aside, I recall Andrei Alexandrescu making the argument that GC is important for type safety, unless the type system can prove that dangling pointers are impossible.
I remember seeing a talk by Cliff Click where he explains how a small change to the CPU can greatly assist with developing pauseless GCs (though his former employer, Azul Systems, has moved on to implement the whole thing in software, probably at some cost to performance).
Regardless of the undertaking, the question remains -- as we move into a many-core era -- whether we'll find a programming paradigm good enough to take advantage of many cores. I think that whatever the winning paradigm may be, it will require a "really good" GC.
To give the programmer this flexibility, Go must support what we call interior pointers to objects allocated in the heap. The X.buf field in the example above lives within the struct but it is legal to capture the address of this inner field, for instance to pass it to an I/O routine. In Java, as in many garbage-collected languages, it is not possible to construct an interior pointer like this, but in Go it is idiomatic. This design point affects which collection algorithms can be used, and may make them more difficult, but after careful thought we decided that it was necessary to allow interior pointers because of the benefits to the programmer and the ability to reduce pressure on the (perhaps harder to implement) collector. So far, our experience comparing similar Go and Java programs shows that use of interior pointers can have a significant effect on total arena size, latency, and collection times.
Interior pointers may pose a problem for sophisticated GCs. Interestingly, this may not be a deal-breaker (see here: https://groups.google.com/forum/#!topic/golang-dev/GvA0DaCI2...), but because Go relies on interior pointers so much, and because the language does not distinguish between interior pointers and regular object pointers, this may prove to be a huge problem in practice.
At the moment they can run on x86_64 but its not as fast as it could be. The have a special build for every linux distribution.
I am not sure how good this all works if you have very few cores and not a lot of threads. All in all however the C4 defently shows the way of the future. Hardware developer should learn from Azul, the OS guys should work more on supporting managed runtimes and the programming language community should do so to. Managed Memory is the future exept in very, very special cases.
Azul's GC is basically a "really good" GC. It can handle hundreds of CPUs with hundreds of GBs of heap with tens of GBs per second of allocation rate with pause times of 10-20 milliseconds. It's basically a solved problem with the proper overall system design.
Rust's type system can.
I really like Rust, but I wonder how it is going to get over this hurdle that previous languages like Cyclone never fully solved.
I would need to have read Andrei's original argument to be sure, but I'm guessing it was along the lines of "without a strong type system, garbage collection is necessary to prevent dangling pointers". And Rust's type system is strong enough to prevent dangling pointers; they cannot happen unless you are deliberately dropping into `unsafe` blocks. This is true regardless of if you have garbage collection built in. But that doesn't necessarily mean that the type system is powerful enough to encode all the patterns that GC allows.
This is quite a strong assertion. What other GCs have you evaluated and rejected as not-really-good? Does this include GHC's parallel generational GC, or BEAM VM's per-process GC?
ETS acts like shared memory, but is not, in fact, shared memory. ETS tables are kept in a separate memory region from process heaps, and aren't automatically garbage collected. They are destroyed when their parent process terminates, or manually.
Per-process GC is a natural consequence of the way systems are built in Erlang, which often involves using hundreds of thousands of short-lived processes. This decreases a system's GC load, as entire process heaps can simply be destroyed when their process terminates, without needing to be traversed. There's also no need for a stop-the-world GC pause.
The Erlang GC cannot be compared with Java's GC. It is not "better" or "worse". Erlang's GC wasn't designed for better throughput but to ensure complete isolation. Erlang values isolation over everything else because it is at the core of Erlang's fault tolerance philosophy. In Java, a task could trigger a collection that could potentially block all other threads. This can't (I think) happen in Erlang. But the JVM's GC has many other advantages.
I'm not familiar with this term, and it doesn't seem googleable. Can you give a definition?
> The Erlang GC cannot be compared with Java's GC.
And yet, by saying "the only really-good GCs are found in the JVM", you compare all GCs.
Sorry. I meant multi-object transactions. Slip of the finger.
And let me qualify: good GCs that are relevant to solving that problem GCs are essential for, namely concurrent data structures. Erlang doesn't have a GC that's relevant (I really like Erlang, BTW).
I actually don't like Erlang as a whole, because it lacks a static type system, but that's besides the point.
I reject characterising the BEAM VM's GC as not-really-good, especially in concurrency context. I believe the immutable, shared-nothing, message-passing model is a superior solution to the data problem than the threads-and-locks model. Actual memory sharing is an optimization which can be applied by the runtime system, as it's done for binaries in Erlang.
And the ghc gc / rts are getting better every release. I'm hoping to spend some time late summer / early fall evaluating how to get better Numa support in. (And jvm notably lacks any Numa story as far as I can tell)
http://docs.oracle.com/javase/7/docs/technotes/guides/vm/per...
http://pic.dhe.ibm.com/infocenter/wasinfo/v8r5/index.jsp?top...
-
But then the next step of work is scheduling work in a locality aware way. The libnuma lib in Linux and the portable hwloc lib that's part of the openmpi project are example substrates.
Admittedly, for most applications this second step won't make a large difference, but in my application domain, numerical computing, better memory locality at any given granularity can yield an order of magnitude improvement.
Locality aware scheduling in this context can actually yield a slightly super linear speed in the multicore / multi socket regime because of vagaries of a systems memory bus topology.
Ill hopefully be in a better position to quantify this last remark more precisely middle to late summer, assuming Wellposed's numerical product launch results in signing some early customers in the next 2 months.
I hope the next version of Rust will have a proper Windows package, though. I don't understand why they don't bundle the version of MinGW they depend on. I was pretty disappointed to be greeted by missing DLL errors after running Rust's Windows installer.
There are countless MinGW builds, with different thread libraries, exception models, etc. I guess when they say "MinGW" they mean the binaries from the original MinGW project. However those get updated all the time. Thus the packages the MinGW installer would download today might not be compatible with the Rust 0.6 binaries which were build some time ago .. probably with a different version of said packages. Thus they should really release a complete package.
Even if there is no compatibility issue, it is just bad policy. I actually program C/C++ on Windows AND have a MinGW build on my machine.. but not the one Rust needs. Which is common because other builds are much more up to date and full-featured than those from the original MinGW project. And I really didn't feel like doing an additional MinGW installation just to play around a little with Rust.
I admit I am ignorant here because I never cared. I know that many academic/niche programming languages bundle MinGW/depend on a MinGW or even Cygwin installation.. I always assumed that was to cut some corners. I mean, I have used multiple programming languages on Windows which compile to native code and have no dependency on a C compiler.
I'm not 100% sure, but I do know that zero.rs requires 'the following libc functions: malloc, free, abort, memcpy, and memcmp'. So I'd imagine it's that.
I have not yet looked at Rust's code, but I guess it is required for some bootstrapping code.
If targeting direct hardware, any CS student should have learned how to implement a memory allocator on their compiler design, data structures or OS classes.
Back when I graduated, this would be a home work exercise.
If you want to implement a high performance memory allocator that works under pressure in a multi-core context, then yes it is complex to implement.
Now something that allocates/releases memory with a general purpose algorithm? That is quite simple and many compiler design books even provide such examples.
Not really, there's no such thing as malloc/free syscalls. On Linux, you have mmap/munmap (at page granularity) and brk/sbrk (which only extend the current heap). You could implement malloc as either a mmap() of one or more pages, or by using sbrk() to extend the heap by exactly the memory you need, but then freeing that memory becomes much harder.
http://msdn.microsoft.com/en-us/library/windows/desktop/aa36...
Should I write a full tutorial on an HN post how to write memory allocators when anyone can easily search for one?
No, but a memory allocator is more that "simple wrappers". My point was that OS kernels (Linux/BSDs at least) don't actually provide malloc/free as syscalls, and that you need to implement an allocator in userspace if you want these functions (even if it's a trivial allocator that wastes memory).
malloc/free are part of the C language runtime, not OS syscalls.
A compiler writer can build the memory allocator on top of what the OS provides as syscalls or take a shortcut and use the C runtime library instead while adding a dependency to it.
I never mentioned malloc/free were syscalls.
None of those four functions use syscalls, nor are any of them simple to implement efficiently in assembly.
Consider that memcpy, under GCC,
* generates different code based on any known alignment of the source or destination
* generates simpler code if the size is known at compile time
* generates only register accesses if one or both of the arguments live in registers
* is elided entirely if GCC determines that it may do so safely
The other three have similar complex behavior. The Rust developers didn't use them just because they were too dumb to know how to write a for loop in assembly.
Anyone with compiler development experience can implement them without much trouble, hence simple.
Sadly that is a skill many seem to lack nowadays.
They can grok HTML5 page full of JavaScript stuff, but then mix language with implementation, think that strong typing is only possible with VM based languages and faith at the look of Assembly code.
Msvcrt.dll began shipping as part of the OS around the Win2K/XP timeframe, IIRC.
This is 100% correct, many of the real syscalls are located in Ntoskrnl, while kernel32.dll is actually part of the Win32 subsystem, given the personalities feature from Windows.
So you could be using another subsystem.
However, given that the POSIX and OS/2 subsystems are no longer supported, the only available subsystem is Win32.
Metro is another matter as it uses another architecture not based on the subsystems mechanism.
But in the end these are all OS APIs, not language runtimes.
Bootstrapping.
It sounds like ~ pointers are basically like unique_ptr in C++11 or scoped_ptr at Google (http://google-styleguide.googlecode.com/svn/trunk/cppguide.x...). In practice, I find that these are the most commonly useful semantics, and as Patrick mentions it is simple and predictable.
I fully agree that the landscape of GC/refcounting solutions is diverse. If indeed there is a way to allow for different approaches without favoring one, I think that would definitely make Rust more widely useful and future-proof.
One question: one distinguishing factor of GC (vs refcounting) is the need to "stop the world" during the mark phase. Would there be a way of doing this from the standard library, or would the GC scheme itself be implemented outside the language? Likewise with any barriers that might be required for mutations, scanning the stack, and other tricky parts of implementing GC?
That's a distinguishing factor only in practice. In theory there are real time garbage collectors.
And I was under the impression that there already are GC implementations that never go into stop-the-world phases, like Azul's Pauseless GC [1]. There's also the new G1 from JDK 7 that promises to make pauses fewer and more predictable than CMS, while avoiding many memory fragmentation issues that CMS has (how well that works out in practice, remains to be seen, as it's not fully matured at this point).
Btw, I love how Rust is trying to solve memory management issues by reinventing the problem. That's what new languages should do, instead of reimplementing other languages poorly.
[1] http://www.artima.com/lejava/articles/azul_pauseless_gc.html
The right language + a good GC can give you much better bounded worse case guarantees, with Azul's as the best I know of for an imperative language (the more functional, the easier it is to do a variety of things).
I am not mega familiar with all of the details of {unique,scoped}_ptr, but my current understanding of ~ is this: basically, the compiler inserts a malloc before and a free after something declared with ~ goes into and out of scope, and it's the only pointer allowed to that memory.
Not quite true with borrowing (i.e. & and &mut), which allows a temporary pointer to the memory to be created. Unlike C++ however, the compiler makes sure all borrows are safe via a fairly intricate borrow checker, that guarantees (among other things) that the borrowed pointers don't outlive the original object. (i.e. no dangling references.)
I can't answer the rest of your question, but note that Rust doesn't allow shared memory between tasks. Therefore, in the worst case, any garbage collector would only need to "stop the task" to mark, while the remainder of the tasks proceed uninhibited.
http://en.wikipedia.org/wiki/Escape_analysis
Most short-lived objects can be stack allocated. Then the heap gets much less of a work out.
The runtime can figure out heap vs stack. Progressively better over time. Definitely better than I can do.
C# has (had?) the option of explicit stack allocated data (pseudo objects). Terrible idea. Premature optimization that prevents the runtime from doing a better job.
If I was doing embedded, realtime, or kernel dev work, I'd want to fiddle the bits myself. But I don't so I don't.
FWIW, Azul has presented to our local user group (seajug.org) a few times, most recently Nov 2012. There's video http://www.nimret.org/seajug/index.jsp?p=2012%2Fnov%2F By all accounts, their allocation and garbage collection implementations are the best available.
In the late 90s, I cowrote OpenGL bindings for Java, which became the model for jogl and so forth. Whereas other bindings using JNI duplicated the native/C structs, I just carried around the native pointer as a long and used it as an "opaque" pointer. Much less copying (thunking) back and forth.
Not really true: when you get into higher order control flow it's a huge mess and usually escape analysis falls down. The Rust lifetime system allows stack allocation to a greater extent than any escape analysis system I know of.
That said, we've thought about doing escape analysis, but usually you don't need it in Rust: people reach for the garbage collector only when it's absolutely needed anyway, so having the compiler promote GC allocations to stack allocations automatically would only help in a tiny fraction of cases.
Everything that would be stack allocated will die in the first generation, and Cheney's algorithm is indifferent to the amount of garbage.
Dont take my word for it, they guys from Azul threw it out of the code completly and replaced it with something they call 'escape detection'.
“What is Azul’s support for Stack Allocation?”
Azul’s support for stack allocation is really support for Escape Detection – fast detection on when an object is escaping a stack lifetime. Sun has been trying to add a classic Escape Analysis (EA) to HotSpot for quite some time. Its not on by default now. Research from IBM some years ago showed that its really really hard to make EA effective on large program, although it works really well on lots of microbenchmarks.
I cant give perect references right now. Search the internet for Cliff Click and Gil Tene. Specially the talkes.
I remember that on there own hardware the do 'Escape Detection' with costum hardware but the think it would be a win in software as well. I dont know if the have it in software with there new x86_64 VM.
Am I misunderstanding the thrust of this article?
Edit: deleted comment about cycles, since they are discussed a bit at the end.
I've never seen refcounting overhead show up in callgrind, so I think the choice to uniformly use the most general version is OK.
Reference counted pointers are not going away.
I don't care if Java programmers don't know the difference between the stack and the heap, please keep the @ syntax to save me from having to type
void f(std::shared_pointer<my_namespace::my_object_type> const & p)
another 50,000 times in my career.Perhaps it happens sometimes, but compare that to leak-it-all-as-garbage and scan-all-the-address-space style collection.
~ could be named unique_ptr and @ something like shared_ptr. Genius!
Looking forward to writing: "let mut foo = upt .."
Maybe I'm exaggerating a bit, but it really seems to work well after using it for a little.
http://www.astrobe.com/default.htm
But having Rust as option is also great.
FYI, object allocation and de-allocation for short-lived objects is much faster with a good GC than with dynamic allocation, and even for long-lived objects GC gives a higher throughput than malloc/free. The problem with GC is latency issues (pauses) when maintaining very large heaps with many long-lived objects.