64 comments

[ 2.4 ms ] story [ 122 ms ] thread
I cannot know for sure but this seems like a post of someone trying to write rust like you write a different language. The idioms are different and if you stick to your old routine it won't work like you would want to.
And the reason you can't know is that he says nothing concrete, in the sense that none of his criticisms are grounded in examples.
The "message-only parallelism oops we need shared memory" thing seems to be a common problem - at the beginning every language thinks they don't need the hellish headache of shared memory. This is not new - Ada went through this like 25 years ago.
Yeah, we went through the same thing. But what's curious about this post is that Rust had safe shared memory for almost a year at the time it was written.

I actually think one of the most interesting parts about Rust is its shared memory story: if you use mutex-protected shared memory, the compiler enforces that you take the locks properly, eliminating data races.

> the compiler enforces that you take the locks properly, eliminating data races.

I assume you don't mean the compiler prevents deadlocks, but I'd be very happy to be proved wrong.

Here's a rather old post that describes the kinds of races that the Rust compiler prevents:

http://winningraceconditions.blogspot.com/2012/10/what-is-da...

"[...] Rust's type system guarantees that concurrent tasks cannot share state but instead must use message-passing to communicate, which precludes the possibility of data races completely by enforcing happens-before relationships on all data accesses (or in the case of this post,[1] by enforcing mutual-exclusion relationships). Yet it's still possible to write nondeterministic programs in Rust (using select2, failure propagation, etc), and so race conditions are still possible."

...Though I'm not sure if this is all that pcwalton is referring to.

[1] http://winningraceconditions.blogspot.com/2012/09/rust-4-typ...

Yeah, we don't prevent deadlocks. Stylistically, we try to avoid mutex locks for this reason, but sometimes you need them, so they're there.
Unsurprisingly, I disagree with most of this post.

* The part about Rust having no shared memory is just wrong. Rust has a very rich set of concurrency primitives at this point: you can use immutable shared memory (Arc), mutexes (MutexArc), reader-writer locks (RWarc), and atomic variables (AtomicInt and friends). And if you're willing to drop down to unsafe code, you get the full set of LLVM concurrency primitives.

* The complaint about smart dereferencing strikes me as odd. Almost every new systems language—Objective-C, D, Go, and Rust (and also Nimrod I think?)—does the same thing that Rust does in that the "." operator also works on pointers. It's type-directed: if you don't know whether you're working with a pointer or not, just go look at the type of the value you're working with. If you don't know a value's type, then you have much bigger problems than knowing whether you'll get an extra memory access on ".".

Hiding the difference between a memory access and a cheaper operation is something compilers have done ever since the invention of register allocation.

* I don't know what the complaint about "bugs with small objects" is, but all word-sized objects were made into LLVM Values a while back, making us as efficient there as you can be. Unless I'm misunderstanding the complaint.

* Doing code duplication to accommodate different types of smart pointers doesn't work. The smart pointers have different semantics: that's why they exist in the first place. You can't just copy and paste your code.

* The GC in Rust does need work. But so does C++'s GC and nobody is saying "C++ is not fast" because of it.

About the only part I agree with is is "fork-join parallelism and work-stealing should be better supported". This is an area we'll need to flesh out more fully at some point. Note that there is now a much more advanced work-stealing scheduler. (It's not using the most efficient data structures for work stealing yet though.)

> The GC in Rust does need work. But so does C++'s GC and nobody is saying "C++ is not fast" because of it.

C++ doesn't have a garbage collector, so I'm not sure what you are getting at here.

Sure it does, here's one:

http://www.hpl.hp.com/personal/Hans_Boehm/gc/

Rust has a GC in the same sense that C++ has one, which is to say that its GC can be provided by libraries rather than being baked-in to the language.

Rust has GC that is part of the standard runtime / library with special language syntax to go with it. C++ has no such thing.
The special language syntax is being removed. Its now just a library.
I'm about to blow your mind here: Rust has never had a GC. Managed pointers were always backed by reference counting, and the cycle collector stopped working sometime in 2010 and was never brought back online. Note that I said "were backed by reference counting" (emphasis mine), because managed pointers no longer exist in the language (unless you turn on the backwards-compatibility flag for them, which is only being provided temporarily).

So there's no longer even special syntax for a theoretical GC. Instead the plan is to greatly expand our support for custom pointer types in general, which will make all user-provided pointer types (implemented in whatever libraries our users come up with) first-class citizens. Our own stdlib will include several pointer types of this nature, including: `Rc` (reference counting), `Arc` (atomic (thread-safe) reference counting), `RWArc` (memory-safe Arc for mutable objects), and, yes, a `Gc` type.

And while it's true that we may provide hooks into the runtime to allow the `Gc` type to be implemented more efficiently, any user library will be able to use these hooks to implement its own GC. There won't be any implementation in the language itself.

(comment deleted)
C++ has ways of dealing with memory and garbage (i.e. memory no longer in use), such as `malloc()` and `free()`, or reference counting, or destructors, and not all of these are as fast as you want them to be in every situation (e.g. see the numerous malloc implementations), so in performance-critical applications/operations, you have to be very mindful of memory use and optimize correctly (e.g. by using memory pools).
He may be referring to shared_ptr, which implements reference-counted garbage collection in the standard library.
Both, really. Neither shared_ptr nor Boehm are perfect, but that hasn't stopped C++ from being fast in practice.
Boehm is far from "fast in practice".
A few small addendum:

* Objective-C is from 1983, do you? It's not exactly new. ;)

* Nimrod is pretty fast and as the only language even supports soft-realtime applications out of the box. Besides that it feels the nicest to use and has a fat standard library with stuff like Redis. Sorry, I only played with that language, but already feel like a fanboy

* About GC. Yes, that's a problem of all newer languages, but whatever implementation you choose there are advantages and disadvantages and which one is the right one also depends a lot on the language itself. Sometimes you are better off with your big fancy algorithm and sometimes you really just want good old reference counting. But yeah, GC is the big great topic and I am sure it will be for the next few decades.

> Objective-C is from 1983, do you? It's not exactly new

The dot-syntax for accessing object properties is much more recent though (2009?).

Announced in 2006, released in 2007 with Mac OS X 10.5.
It's a pretty unimportant feature though. It's just some very minor syntactic sugar for a regular message send. All uses of obj.property can be replaced with either [obj property] or [obj setProperty:...] depending on whether it's being written or read. It's about as significant as when C added the ability to declare variables in the middle of a block or support for // style comments i.e. not at all.
For now perhaps, but it does seem as though properties are gradually being lifted into a first class language feature rather than a set of conventions.
Properties are, but dot syntax is almost entirely not about properties.
I'm not sure I understand what you mean by that. Dot syntax is a way to refer to properties independent of the usual message sending syntax. How can it not be about properties?
Because... it's not?

Declare a plain getter method:

    -(id)foo;
Call it with dot syntax:

    id foo = obj.foo;
Works fine. Now declare it as a property:

    @property id foo;
Call it without dot syntax:

    id foo = [obj foo];
Works fine. They are completely unrelated and just introduced at the same time. You could take dot syntax out of the language without affecting properties in the least. You could take properties out of the language without affecting dot syntax in the least.

(There is one exception to this, and that is the part where dot syntax understands and calls custom getter/setter names for properties. But that is the only place where they even touch, and it certainly doesn't mean they're somehow integrated.)

If you define 'properties' as only use of the @property directive, then what you say follows.

However that's a pretty arbitrary definition, since properties pre-date the introduction of that directive.

Well no, the wishy-washy "properties are certain kinds of methods that we can't really define in a particularly concrete manner" definition of properties is the arbitrary one. Saying that properties are those things which are declared with @property is pretty non-arbitrary. That's the definition the compiler users, that's the definition the runtime uses, and that's the definition I use.

Using the wishy-washy definition doesn't help, in any case. Nobody would say that e.g. autorelease is a "property", but you can still call it with dot syntax.

I seriously doubt your intellectual integrity here.

You know that the dot syntax is a mechanism that matches with the conventions for property access, and no other access pattern.

You also know that its possible to call zero argument methods that aren't accessors using the dot syntax.

I don't believe that you think that this is the purpose for which the dot syntax was designed, and yet you bring up 'autorelease' to support your position.

As I said, based on your specific definition, you are correct, but using denigration, a false straw-man 'quotation', and dishonest examples doesn't make your definition fit the evidence.

As I said, and you sidestepped, properties pre-date the directive, therefore the directive cannot be the sole definition of what properties are.

You've decided that I must agree with you, and the only reason I could possibly disagree is that I have no intellectual integrity.

Well, how the fuck can I argue against that? Sure, you must be right. Clearly I could not possibly disagree with you for any other reason.

Seriously, fuck off.

You aren't reading what I wrote. I didn't decide you must agree with me. I accepted your point of view and told you why mine differs.

You then went on to argue that your point of view was the only reasonable one.

If you addressed the actual question instead of being aggressive and insulting, we might have had an interesting technical discussion.

Yes but that's pcwalton's point. The OP complained that Rust is slow, in part, because of that feature and pcwalton pointed out that A. most languages do it now and B. that it has no runtime cost because it's just sugar syntax.
Sure. And I'm just saying that it doesn't really matter what year ObjC got them because they're not a big deal anyway.
Glad I wasn't the only insta-convert to Nimrod! Such a neat language!
It does look very interesting. I was surprised to learn that it cross-compiles to C before being compiled. I'd be even more interested if there was an IntelliJ plugin for Nimrod.
Why? That's a fairly common strategy, GHC worked the same way until a few years ago (and IIRC you can still ask for compilation to go through C).

It makes for easier reading of the codegen (reading C source is easier than reading machine code, even if it's generated C source) and before LLVM made it much easier to leverage C compiler optimizations & multiplatform support.

> Nimrod is pretty fast and as the only language even supports soft-realtime applications out of the box.

Rust has a richer set of safe memory management primitives than Nimrod does (Nimrod is reliant on non-thread-safe deferred reference counting with stack scans, while Rust has thread-safe unique ownership and thread-safe RC as an option), so I'm not sure I agree that Nimrod is the only language that can do soft real time. It's definitely a neat language though.

> Nimrod is pretty fast and as the only language even supports soft-realtime applications out of the box.

This catches me. Especially for soft-realtime stuff. Because I am looking for strongly, explicitly typed in-game logic language - which needs deterministic and symmetric coroutine.

I have to check it out.

2D or 3D, out of curiosity? I'm just getting my feet wet playing with game programming (a 2D engine, and likely using Nimrod itself for the game logic as well as engine) in nimrod, and am going to be blogging about it.
3D. I don't think there's really a a lot of difference… Anyway it's just a small project, not a block-buster.

Graphics part is already mostly written in C/C++. I won't use immature languages for low-level system due to library support and debugging ability

I am still reading the manual, but Nimrod doesn't seem to have coroutine support. Do you have any idea?

No coroutines per se, but it does have really nice native threading support, and I'm nearly certain that coroutines could be easily put in userspace. It's a powerful language in that regard.

Ask the guys and gals in #Nimrod on freenode, they're super Super helpful -- I'm not well versed enough in parallelism and concurrency to answer your question, but they are!

The big problem for me is the compilation time, which is very slow (this may or may not be because the compiler itself is written in Rust).

I dedicated a few days to hacking on rustc this summer, and while the tooling and community are very nice, the compilation times are really high. Git pull && make ? 20 min. Edit a file && make ? 5 min. IIRC the major was that metadata got serialized/unserialized all the time, but anyway it's one of the priorities. I'll hack again on it with pleasure when I'll have more time.

> this may or may not be because the compiler itself is written in Rust

Well, due to the nature of self-hosting compilers, it's certainly greatly exacerbated by being written in Rust. :)

To wit, a self-hosting compiler must actually compile itself no less than three times to ensure that the generated artifacts "converge" upon a single point. This is as true for Rust as it is for GCC.

Three times, so:

    1. Old version compiles new version
    2. New version, compiled by old version, compiles new version
    3. New compiled by new-compiled-by-old compiles new version
I hadn't heard of this before and initially didn't see why #3 is special. But I guess the idea is, the output object code is a function of both the input source code and the compiler version. And those two are the same for #2 and #3. So you expect the output of both #2 and #3 to be identical, and if it's not, that's a bug in the compiler. Is that how it works?
Yes, the third compile is effectively a single very large test.
And Go is even slower, so what? If you want to squeeze out every little bit of performance, use C and not a fairly young, mostly unoptimized, heavily changing, not-yet-1.0 language.
He complains about message-passing being prone to races, when earlier in that same paragraph he complains about the lack of shared memory between tasks? It sounds like he has some legitimate concerns, but this blog post reads more like a rant than a considered criticism.
> This post is written from my experience with Rust in May 2013.

This post is based on an experience with a version of Rust with the old runtime, with pervasive internal iterators, with managed pointers (which we haven't recommended using for years, and are in the process of chucking out), and is missing all the perf work we've achieved over the past six months. Suffice to say, any experience with Rust (especially in terms of performance) from that long ago is so dated by this point as to be completely unrepresentative.

That isn't to say that Rust is fast yet, because we refuse to say that until we're as fast as C++. But a headline like this seems needlessly sensational (perhaps consider "Performance pitfalls in the implementation of Rust 0.6").

(comment deleted)
Oh, look, my rant finally got noticed. ;)

First off -- yes, it is a rant. I actually had some more nuanced in-person conversations, but those aren't nearly as fun to post. So you get the version that was fun for me to write. Sorry.

That said, I'm glad to hear Rust is all wonderful now. It sounds like pcwalton agrees with most of my points and got them fixed. Kudos. I look forward to better GC, fork-join parallelism, and work-stealing.

It doesn't seem like anything significant was done with the type system, though. How does Rust handle "find_and_insert" now?

If folks are interested in actually critiquing code style, feel free to check out the rusty-turtle code at http://cananian.livejournal.com/68747.html. I disagree that I was trying to write Rust as if it were some different language -- my frustration was in part because I was trying to write things with Rusty ownership types, etc, and getting that to work properly was so frustrating (and the error messages so opaque). If I just stuck managed boxes around everything my life would have been simpler -- but then I would not have been programming in Rust, really.

I'm interested to hear more discussion on the primary point made in my blog: is Rust supposed to be 'safe and fast'? Or just 'safe'?

> I'm glad to hear Rust is all wonderful now.

Nope, we're not saying that yet, Rust still needs a ton of work!

> How does Rust handle "find_and_insert" now?

Google doesn't return any obvious results for this string, what is it supposed to do?

> is Rust supposed to be 'safe and fast'? Or just 'safe'?

If Servo (written in Rust) isn't faster than Gecko (written in C++), then Mozilla won't continue funding Rust. Rust needs to be fast in a very existential way. :) And it will be! Initial benchmarks of Servo's performance are incredibly promising (pcwalton could go into more detail here), and there's still bushels of low-hanging fruit to be plucked.

>If Servo (written in Rust) isn't faster than Gecko (written in C++), then Mozilla won't continue funding Rust. Rust needs to be fast in a very existential way.

Ouch, I didn't know the project had a deadline. By when does it need to be faster?

kibwen is not speaking for the Rust team.
Indeed I'm not! Just an over-enthusiastic community member here. :)
Ok, that makes me feel better. I've been watching and waiting for Windows support to get better, and I'd hate to have it fizzle before I really got to play with it.
re: find_or_insert

http://static.rust-lang.org/doc/0.8/std/hashmap/struct.HashM...

It appears to still have the mandatory copy. (And this is just one example of a pervasive issue with the standard library.)

If that's the problem, it's really rather easy to define a wrapper that only clones on an insert. (And, I fundamentally disagree that over-copying this is a pervasive issue; there are a few places (mainly old code) where unnecessary Clone-ing is performed, but this is very much the exception.)

  fn find_or_insert_clone<'a, K: Clone, V>(map: &'a mut HashMap<K,V>,
                                           key: &K, val: V) -> &'a mut V {
      match map.find_mut(key) {
          Some(x) => return x,
          None => {}
      }

      // `key` is definitely missing
      map.find_or_insert(key.clone(), val)
  }
> I actually had some more nuanced in-person conversations, but those aren't nearly as fun to post

Meta: I'm pretty disappointed with this sentiment. I would have loved to read those discussions instead of a rant. They seem far more intellectually honest.

Sorry, I gave Mozilla about 3 weeks of free work, then travelled to SFO and talked with Rust folk in person. That's a lot of donated time. I was working on another project by the time I wrote this post (from my written notes). Try not to look the gift horse in the mouth.
I don't care performance a lot if it's sane enough.

At least for me, I would use Rust if it supports (1) safe and nice symmetric coroutine with automatically growing stack (2) devices for correct programming - such as const-correctness. (3) a device guarantees safe communication between threads - like Go channel. (4) stable implementation. (5) full support for C interop.

AFAIK, Rust now has all except #4. So I am waiting it to be stabilized.

Actually I want something like C++ with symmetric coroutine, but D is too unstable for coroutine…

I've been using Rust for finite element method research, and I've been pretty happy with the results, the code is faster than my previous effort in Julia. That may be somewhat of a second system effect, though. I had trouble with detecting in Julia what the effect of the garbage collector was, I don't know if that's improved or not. The code in Rust has no shared/gc usage at all, so I'm pretty sure what the effect of garbage collection is :)

One thing I like about Rust is that it makes doing allocation-free performance-critical calculations somewhat easier and safer. This is because I can preallocate my data before critical loops, inside the constructors of implementation objects, then hand out borrowed, non-mutable pointers to this data to clients from these objects. That way the clients don't have to pass in buffers to use, which is none of their business -- it would break encapsulation for them to even know what kind and how large of buffers to pass. The clients cannot detect that they are actually receiving a pre-allocated, shared buffer.

In other languages that would ordinarily be very dangerous without extra effort, because if they or some other code turned around and called routines that used that same pre-allocated buffer again, then the data would be silently written over, in an "action at a distance" way which would be a nightmare to maintain.

But Rust "freezes" the contents behind the immutable borrow, making sure that it can't be borrowed mutably again and thus cannot be changed, while the immutable borrow is in use, from anywhere in the program, all resolved at compile time.

This allows getting very aggressive with the pre-allocation strategy.

This is fascinating. If you ever have the time, would you be willing to post this along with some of your code to the mailing list (https://mail.mozilla.org/listinfo/rust-dev)? We love getting informative feedback like this. And if you have any suggestions for improvement, we entertain those as well. :)
I'd be happy to, I'll make the code somewhat presentable and post a message to the list sometime soon.