Hacker News new | past | comments | ask | show | jobs | submit login
The Rust Programming Language (dropbox.com)
170 points by jamii on Sept 22, 2011 | hide | past | favorite | 79 comments



I'm writing this comment in hope that Rust language designers read this thread.

It's very important to language designers at this time to avoid stop-the-world GC approach in Rust at any cost.

Google Go is beautiful programming language, but its key limitation is stop-the-world approach to GC which makes it very difficult to implement highload web servers, DB engines and other software with strict max-time-to-response requirements. Recent research [1][2] suggests that "normal" Go program spends more time collecting garbage than performing actual calculations.

[1] https://days2011.scala-lang.org/sites/days2011/files/ws3-1-H... [2] http://www.theregister.co.uk/2011/07/01/go_v_cpluplus_redux/

Go programming language has gone too far to separate owning and non-owning pointers at syntactic level, to disallow changeable global variables, to enforce rules like "each mutable object is accessible from only one thread", and introduce other means to avoid GC.

But Rust is new and young, already has 2 types of pointers, and it could avoid GC by RAII and reference counting. Objects linked in cycles with owning pointers would cause memory leaks, and I think this behaviour may be left as is, kinda "it's not a bug, it's a feature". Weak pointers could point to intermediary descriptor which holds owning-refcount and nonowning-refcount. Weak ptr behaves like a NULL if owning-refcount is zero and target object is already destroyed. This is to guarantee safe memory model.

In short please take all means to avoid main Go defect "Garbage collection takes more CPU time than actual computations". Please avoid stop-the-world GC approach which makes the language almost useless for real applications.


I'm not sure that your premise is solid. Your citations point to a single program that was somewhat naively written, not well optimized, and designed as a computational benchmark spending much of it's time in GC. That's doesn't necessarily imply that "GC takes more CPU time than actual computations in normal Go programs". That's not to say that Go's GC isn't slow, but as citation #2 shows, one can limit heap allocation if performance requires it.

So, the fact that you're saying "Don't do what Go does, it makes Go almost useless for real world applications" seems odd to be because:

1. Not all applications care deeply about stop-the-world gc pauses.

2. Not all of those that do will have a problem with them, as GC can be avoided by value semantics and escape analysis.

3. Go will not necessarily always have a Stop-The-World GC, though it does now. I don't think it's required by the spec.

4. People are using Go in real world applications, implying that it isn't almost useless. Even servers.

5. The Rust folks clearly have thought hard about efficient and scalable memory management, and the slideshow indicates that, so I'm not sure why you're worried for them.


Garbage collection in Rust is thread-local. We never need concurrent GC; this invariant is enforced by the type system.

Leaking cycles is not really an option, sorry. At the very least we're going to have a cycle collector. It's too easy to make cycles, especially when you have closures (all you need is to close over an object that contains a pointer to the closure itself; this is very common for e.g. event handlers).

Besides, a lot of the problems you state can be addressed with generational or incremental garbage collection.


If there is no difference on the syntactic level between owning and non-owning pointers, like in Go programming language, like in Python, then I agree with you - it's extremely easy to make a cycle.

But if there's a difference between owning and non-owning pointer, then backlink to parent object in child object could be non-owning and other issues could handled in similar manner.

Cycle detector which stops the program with references to the source code where the cycle was created - is a good idea, because in server systems under heavy load it just could be disabled. Basically it's a debugging tool, once it was proven by the testing that there are no bugs, these checks can be turned off in production. Like a C assertion checks.


That's more that go has an incredibly crude GC (which is in the process of being replaced, as far as I know), and less that it is fundamentally broken.

Other language and GC implementations with a large shared heap can work fine. See, for example, Azul systems' JVM.


Azul systems GC has Hardware and OS support that you don't have normally but I do generally agree.


They have x86 ports of their system as well, I believe.


This is not really correct. As far as I understand the run a virtualization layer ontop of x86 that provieds these features. On would think that this is not worth it but there JIT and GC are so much better that it is worth it.

This however is not as easy to use as typing "ruby dostuff.rb" or something. You need to run a full virtuall OS.

I do really really hope that Intel does these Hardware changes it would help ever language with GC. (The OS would need to support that too but thats probebly a easyer then getting intel to add the feature)


They are following the Erlang model, in which GC is per-thread only, and threads are small lightweight language-level creations. So a GC pause in one thread doesn't affect other threads in the program.


It would be nice if it could work that way in Rust.

In Google Go garbage collector stops all heavyweight OS threads [See go/src/pkg/runtime/proc.c:runtimeВ·stoptheworld() in the source tree.]


It does indeed work that way in Rust. It's also intended that the GC be able to yield in the green thread sense as well, so it doesn't even lock up a core.


Reference counting is not the answer. It has too much overhead and cycles are a serious problem that shouldn't be swept under the rug. Real GC can be pauseless with the right language design and the proper use of virtual memory. I don't think any open source project is there yet, but there was a very interesting article a while back about the Azul JVM's approach.


Reference counting works just fine in Python and C++.

If reference counter is thread-local (i.e. its increment/decrement is not done with InterlockedIncrement/Decrement = LOCK XADD/XSUB), then it won't cause a lot of MESI traffic and related memory I/O interlocks.

And, as far as I can see, refcounting in Rust can be thread-local.


You'd be surprised how much overhead our reference counting has. 10% of the time of rustc, last I measured, was spent adjusting reference counts.


In Go, there's much larger overhead for GC.

Russ Cox says in [2] (see link in my first comment above): We used gopprof to study an inefficient Go program and then to improve its performance by an order of magnitude and to reduce its memory usage by a factor of six. A subsequent comparison with an equivalently optimized C++ program shows that Go can be competitive with C++ when programmers are careful about how much garbage is generated by inner loops.

They got a speedup about order of magnitude by improving memory management in the inner loops. So 10% of time for refcounting isn't that much. 90% of time for GC is much worse.

Especially if it is a stop-the-world GC on 12-core server system. One core collects garbage, 11 cores just cool off.


Your conclusion is flawed. We got an order of magnitude improvement by making a naively-constructed program efficient, and it was easy. That's hardly an indictment of Go's garbage collector.

Also, the Go GC is a parallel GC which will use all cores during collection.


Sometimes I wonder why do all the programming language designers prefer to invent several different syntaxes for one concept...

From the third slide: we can see two different function definition syntaxes, a "normal" style and a "ruby" style. I really like the way of JavaScript, CoffeeScript and OCaml that have a single unified function syntax.

Also on this slide: why have a special syntax for importing submodules from modules/crates? Again, CoffeeScript excels here, an import is simply a variable declaration:

  {int, vec} = require('std')
Furthermore, it has always bothered me why generic types use <> brackets. I can understand it in Java and C# (where types are declared before values/functions), but in Rust, it's really unnecessary to introduce the 4th type of brackets into the language (apparently, they used [], but latter changed it to <>... I would really like to know why.)


"From the third slide: we can see two different function definition syntaxes, a 'normal' style and a 'ruby' style. I really like the way of JavaScript, CoffeeScript and OCaml that have a single unified function syntax."

Functions declared at top-level with the |fn| keyword can be mutually recursive and don't close over anything by default. Blocks, however, are unique in that they can close over stack variables (we statically ensure that they can't escape) and can't be mutually recursive. At the moment, blocks can only appear in function argument position. So, although both |fn| functions and blocks are functions in some sense, they have different enough semantics that we felt that different syntax helped to convey the distinction.

"Also on this slide: why have a special syntax for importing submodules from modules/crates? Again, CoffeeScript excels here, an import is simply a variable declaration:"

Well, in CoffeeScript, imports are actually statements that are evaluated at runtime. In Rust, they're compile-time constructs. Furthermore, using a separate syntax gives us more power — you can say |import *|, you can say things like |import std::{hashmap::map, vec}|, etc.

"Furthermore, it has always bothered me why generic types use <> brackets. I can understand it in Java and C# (where types are declared before values/functions), but in Rust, it's really unnecessary to introduce the 4th type of brackets into the language (apparently, they used [], but latter changed it to <>... I would really like to know why.)"

Because you can use generic type instantiation in value position as well (e.g. none::<int>), and [] was preventing us from using [] for array indexing (previously, you wrote v.(0) for v[0]). I also thought it'd be more familiar for C++, C#, and Java programmers.


> {int, vec} = require('std')

I don't know coffeescript, but this looks to me like "std exposes two things, and I am bringing them into this namespace as int and vec". Whereas the rust code looks like "std exposes int and vec, which I am bringing into this namespace, and possibly other things which I am not".


What this means, in CoffeeScript, is that std exposes int and vec, which I am bringing into this namespace, and possibly other things which I am not. If I want to use all of std, I would have to write

  std = require('std')
and access std.int, std.vec and so on.


Thanks for clarifying.

I think I like rust version better: when I see an '=' sign, I assume that the RHS returns something which is assigned to the LHS, and something doesn't depend on the LHS. Coffeescript violates that, and it feels much more like 'special syntax'. But without knowing either language, I won't go further than "this seems weird".


Actually, the genius of the coffeescript version is that it's just a use case of the general destructuring syntax. So

    { x, y } = { x : 3, y : 5, z : 3 }
is perfectly valid as well. Once you get the hang of coffeescript's destructuring, it's insanely powerful.


I don't view it as that the something returned depends on the left hand side, rather that the left hand side picks what it wants out of that. It's also useful in arrays:

  [x, y] = [1, 2]
With splats:

  [first, second, rest...] = contenders


I am not trying to correct but clarify what you mean by unified syntax.

In OCaml you can say let rec func = ... , let func = ..., let func = function |... | _->... and let func = fun ... -> ... Now I find that each of these variations make good sense and would call them unified facets but they tripped me up many years ago when I was first learning the language.


That's true, and there is also let func a b c = ..., and most of these are just syntax sugar, mainly useful at the module level. The important thing is, though, that the function declaration syntax is the same as the "inline" function syntax, I can easily say:

  let f = function 0 -> false
                 | 1 -> true

  use (f)
and then refactor the code into

  use (function 0 -> false | 1 -> true)
That's the consistency I'm talking about...


Even in C++, C# and Java this choice makes no sense.

In both C++ and C# it leads to ambiguous constructs and parsers beyond the realm of yacc & friends.

The Java syntax was carefully designed to avoid those ambiguities, but still...

Simply using [] or {} for template arguments would have been way, way better for everyone.


Slideshare here for those having trouble with the Dropbox-hosted version: http://www.slideshare.net/pcwalton/rust-all-hands-winter-201...


Could you give examples of macros-by-example, as the presentation puts it? I'm not sure how those would work in a non-Lispy context.


Check out the macro tests in this directory: https://github.com/graydon/rust/tree/master/src/test/run-pas...


Maybe I'm just naive, but whenever someone says "systems language", I think "ooh, something better to write kernels in?" I guess I'm waiting for something semantically equivalent to C, but with better options for macros and code generation, and some compile-time guarantees. Just less tedium, more DRY.

I don't actually program C, I just have projects in mind where it seems like that's the best language, where performance and compatibility with other languages is important. But C seems like such a pain to program in that I really wish there was a better option.


Rust has an unsafe sublanguage you'll probably be interested in.

That said, writing kernels is explicitly not a goal of Rust. It may well work great with a custom bare-metal runtime, but we aren't letting the needs of OS kernels constrain our design space at this time.


Honestly, I feel like people are too scared of C, and sometimes I think that derives from ignorance about systems (and not quite the type of systems Rust is supposedly dealing with, I don't think; but I didn't finish the slideshow). Read CS:APP; you'll learn C and systems at the same time, and see why C isn't so scary after all, but systems are. (This is just based on conversations I have had, I'm not assuming this would be appropriate for you, andrewflnr!)


In this case, they're looking to write a large portion of a web browser in this language. That's practically a kernel, these days, and the language looks well-suited to the task.

It's ambitious, definitely, but the world needs more ambitious things that will be amazing if they succeed.


consider D by Walter Bright. he hangs out here too.


The typestate concept seems like an incredibly cool tool for enforcing static guarantees. As far as I'm aware, not even Haskell has anything similar to that (feel free to correct me!)


I'm not clear: is typestate a compile time or run time guarantee? [Edit] That is, is the guarantee implemented at compile time, noting violations similarly to syntax violations, or does it generate "invisible" code that performs runtime checks which can throw something similar to exceptions?


A bit of both, actually. It's mostly compile-time checking, but you can also write assertions about the typestate of a particular variable. The compiler generates code to check the actual values at runtime - if the assertion fails, the program is aborted. The really cool bit is that the compiler can take the assertions into account when compiling code that follows the assertions.


Thanks. When you say "aborted", does that refer to an Erlang-style (micro) process, or the entire OS-level process?


Failure only brings down the current task, not the process.


I don't know.


It's like design by contract (preconditions/postconditions) without classes.

Interesting parallel to how Go has interfaces without classes.


This is probably minor, but one of the things that bugs me is the incredibly shortened keywords. Is it really that much of a slowdown to type "function" or even "func" instead of "fn"? Same with "ret" instead of "return".


The problem is not typing speed, but word size. It fills the screen with garbage and is typo-prone.

Small reserved words means I don't have another failed compilation because I typed "fucntion", or the small but significant mental pain of backspacing.


A huge one for me is that for some reason I can never correctly type the word "Length". As if to highlight my point to myself, I consciously thought about how to type it, and still ended up with "lenght" on the screen.

Python's built in function "len()" is a lifesaver in this regard.


Skimmability also suffers with long keywords.


It is hard.

Each time when I have to define callback inside callback inside callback using anonymous functions in javascript, I wish they chosen something shorthe than "function".


> Same with "ret" instead of "return"

And if we're going to have something so devoid of obvious meaning as "ret" why not go the whole way down to a single character?

Smalltalk which I generally consider to be a verbose language manages quite nicely using "^" to indicate an explicit return-

    ^answer


I like the concurrency trend - channels & cheap threads like Erlang and Go.

The concurrency feature that stands out: ... no need for ... concurrent garbage collection. (slide 17)

The upside should be amazing performance compared to simple GC.

The cost is developer effort for memory management hints (slide 22). We'll have to see if it's worth it.


I'm really looking forward to Rust being somewhat usable, I think it has made almost all the right choices. This is Google Go done right.

As a C++ developer I find my efforts on "memory management hints" to be totally worth it, due to the fact that you actually have to think about object ownership. Which in my mind makes for better designs.



This looks like it's going to be a great language. Compiling to LLVM should mean that it does tall call optimization, which is great and also means that integrating with C/C++ should be fairly easy.

The only thing it's missing that I'd like to see is support for CPS conversion like streamlinejs does. Even if it supports growable stacks that start at 1KB, with thousands of threads CPS conversion is still more efficient. Having said that, seeing as they are writing browsers, not servers, I can see why it's not a priority to do this.


Can you explain how CPS solves the growable stack problem? I haven't heard of this before.


The idea is that you have a small number of OS threads that have their own stacks and you cooperatively map all of the user-space greenlets onto these threads, but whilst the greenlets are waiting to run, instead of storing their continuation as their stack and registers, you store the state as a closure. There's an obvious trade-off between responsiveness and memory usage, but you can adjust this by changing the number of OS threads.

So the stack frames aren't allocated on the heap.


And where exactly are those closures allocated?


They would be allocated on the heap, so mapping a greenlet onto an OS thread is a fairly expensive operation. However, the greenlets are cooperatively rather than pre-emptively mapped, so this doesn't happen as frequently as you may imagine.


> > > So the stack frames aren't allocated on the heap.

> > So where exactly are the closures allocated ?

> They would be allocated on the heap

You're playing a weird game of semantics here. closures and stack frames are the same thing in this case. The fact that the "call stack" is implemented via a register called "the stack register" and uses a LIFO move-the-mark allocation scheme is an irrelevant implementation detail.

Your assertion that CPS conversion is somehow more efficient than growable stacks is not supported by any of the arguments you presented.


Closures and stack frames are definitely not the same thing. CPS conversion is used to create continuations; it's not necessary that the continuations are compiled naively using CPS.


It essentially lets you turn your stack state into closures on the heap which are passed to tail recursive functions.


Stack allocation for call frames is a very important feature of Rust. SML/NJ does heap allocation for all frames and its performance suffers as a result (they have to invest very heavily in a good generational garbage collector, because the nursery fills up very quickly with stack frames). Graydon decided early on that in order to be competitive with C++ we would need first-class stack allocation, and I agree.


but how is that related to CPS? (or closures for that matter?)

If you're allocating the stack frame on the heap, you can just replace the prologue (e.g. x86 intel syntax, cdecl calling convention)

    sub esp, LOCAL_SPACE_NEEDED
with

    push LOCAL_SPACE_NEEDED
    push ARG_SIZE_TO_COPY
    call allocate_frame
(where allocate_frame allocates enough space for storing link to previous stack frame, all args and all locals; then, it copies all arguments, return address and stack pointer, switches stacks to the new stacks and returns from the call)

You can modify the return to call "deallocate_frame" (which would switch stacks back before returning), or you can just make sure the return address on the new stack points to "deallocate_frame_and_return", and use a regular return in place.

[Actually, come to think of it, it's an excellent idea for a stack protection system that can be turned off for speed - the call to "allocate_frame" could be configured at runtime to patch its caller to just do "sub esp, LOCAL_SPACE_NEEDED" for maximum speed. Or it could just check stack overflow; or it could allocate frames on heap for better memory use; and it could mprotect/virtualprotect the edges of this frame for protection]


That's another option for doing it. However, writing in CPS implies that your calls are all tail calls, and they all take a continuation closure as an argument.


I know what CPS is. But Dan states that "CPS is more efficient than growable stacks", and I can't see how CPS plays a nontrivial part in the stack size optimization.


Good call on single threaded language bridging. Multithreading in XPCOM was either bad or worse depending on the phase of the moon. I almost had an important project tank because XPCOM "out" parameters (unfortunately used internally whenever a method had a return value) were not thread-safe. We ended up using a different threading library and only making synchronous XPCOM calls.


I saw the C++ like syntax with std::stuff and stuff<T> and it turned me off.


I saw the ML like syntax with `let r = 42 { stuff };` and it turned me on.

Frankly, your bits of syntax are no worse than `Std.stuff` and `'a stuff` (or `Stuff a`).


See my comment above for the stuff<T> issue.

As for std::stuff, in earlier versions we wrote std.stuff, but it actually hurt readability (not to mention complicated typechecking), since '.' is also used for record field projection and object method invocation. In the MLs it's not so bad because module names have to begin with a capital letter, but we tried to avoid making case significant.


I only have a tiny bike-shedding comment:

I'd expect the arguments of the map-function in the example on page 3 to be the other way around. The 'grades' argument gets pushed down several lines in the current form.


In chrome I am just getting a blank page with some javascript errors. In IE I only can see the right half of the presentation. Mozilla is supporting FF well though :)


Works fine in Chrome (dev channel) here. I was a bit surprise to have load times (spinning wheel & all) between each and every page though.


I just used Keynote's export feature. I'll upload to slideshare.


May I suggest speakerdeck[0] instead of or as well as slideshare?

I've seen a pair of slide decks on it lately and it's been a much, much better experience than consuming through Slideshare: it's faster, more reliable, it looks better and it's not flash.

[0] http://speakerdeck.com/


Just dump a pdf of the slides in your dropbox, less hassle.

Works ok in FF on Xubuntu 11.10 but sloooooowwwwwwww


Seconded. Please just give us a plain old PDF that we can read in-browser or download, instead of making us screw around with all these weird friggin formats.



No errors for me in Chrome 14 on Win7. It is broken in IE 9.


I am running Chrome 14 in Win7... After a little debugging it looks like it was actually my MeasureIt! extension that broke it.


Works great in Opera Mobile on my Android, though.


I tried to fullscreen it, but i couldn't because they intercept keystrokes.

So i fullscreened and then clicked on the link, but then i couldn't get out of full screen or close the tab because they intercept keystrokes.


It looks pretty cool and oriented at making a browser engine. (tl;dr version i guess)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: