How to speed up the Rust compiler in 2018
blog.mozilla.org
blog.mozilla.org
These are all tiny, targetted microoptimizations worth a percent or three of benefit in specific tests. They're worth doing (or at least evaluating) in any mature product and I have no complaint.
Nonetheless rustc remains really slow relative to other similar technologies, including C++ compilers. Is there any consensus as to why?
I mean, with C++, the answer is something to the effect of "template expansion happens syntactically and generally has to be expressed in headers, leading to many megabytes of code that has to be compiled repeatedly with every translation unit". And that isn't really amenable to microoptimization. We all agree that it sucks, and probably can't be fixed with the language as it's specified, and chalk it up to a design flaw.
What's the equivalent quip with rustc? I mean... is it going to get faster (in a real sense, not micro), or is it not? Is this fixable or not, and if not why?
Isn't this what happens with rust too ? It's called "monomorphisation" there but everytime you use a Vec<i32> in Rust I would guess that what happens is fairly similar to a template instantiation.
Compare this to a `map` function which is only generic over the return type:
fn map<T>(self, f: &FnMut(Self::Item -> T)
This function would only be instantiated once per unique T, regardless of how many times you use it with different closures. This halves the number of trivial functions that LLVM need to process and (usually) inline. The reason `map` isn't defined like that is that it uses dynamic dispatch instead of static dispatch.However, I don't actually know how much a difference it would make, but it's my understanding that LLVM was never optimized for the use case where you have many, tiny functions (which is more common in functional languages).
I suspect the real reason is going to turn out more complicated, no? Like, the code is really "dead" yet because we can't know if it will be used later without solving the halting problem.
Which, if you think about it, is pretty much exactly the situation C++ is in. All those expanded templates aren't "dead" yet at the point of expansion either, you just don't know if the code will use them. And in practice it won't, so you wasted a ton of cycles to find that out.
That is only one type of dead code elimination. It is not necessary to solve the halting problem to eliminate larger portions of code than just unused register assignments. There are plenty of ways to eliminate beyond that, such as proving that entire classes, functions, declarations, branches, loop iterations, and data are unreachable. Especially so in a type-resolved format like MIR, some of these higher level optimizations become really cheap and would allow for LLVM to do a lot less work.
That's not really the case in C++. Most code in headers is not compiled repeatedly in every translation unit. The vast majority of it is dead code, which compilers are very good at discarding at an early stage.
> We all agree that it sucks, and probably can't be fixed with the language as it's specified
A lot of the overhead can be fixed, and is fixed, via precompiled headers, which have been around for decades.
> What's the equivalent quip with rustc? I mean... is it going to get faster (in a real sense, not micro), or is it not? Is this fixable or not, and if not why?
At this point, rustc and C++ compiler performance problems mostly consist of little things that add up. As engineers we always want there to be a magic bullet that will solve the performance problems, but in mature technology like compilers, those magic bullets often don't exist.
Ironically, C++ often doesn't suffer from the "too much code" problem as badly, because in practice programmers don't write "modern C++" nearly as much as C++ fans think they do. Industrial C++ uses raw pointers and C idioms all the time, and these tend to compile faster than the equivalent "modern C++" idioms. Rust, on the other hand, forces you into the corresponding "modern" idioms, and the compilation time tends to be slower as a result.
To this end, probably the most fruitful area we can explore in rustc is fast MIR-level optimizations to reduce the amount of code that we send to LLVM. In effect, this would desugar the "modern" style to the "raw" C style before sending it off to the heavyweight optimization and code generation framework.
Ex, there is this infamous error in swift:
"Expression was too complex to be solved in reasonable time; consider breaking up the expression into distinct sub-expressions"
for something like this:
matrix11 = (g(x23(x12+x32)+x13(-x22+x32)-(x12+x22)x33) * sin(a)) / (x13(x22x31-x21x32)+x12(-x23 * x31+x21 * x33)+x11 * (x23 * x32-x22 * x33))
C++ would have no problems compile that expression quickly.
Since rust and swift are similar languages, there is probably a similar issue.
Swift's type checker works quite differently than Rust's. I'm not aware of any exponential time complexity issues in type-checking Rust code.
Are there any languages with fast compilers that aren't designed from the start for fast compilation? When I think of "fast compilation" I think of Wirthian languages.
So it's going to focus on a few micro-optimizations that someone can find by doing a little bit of profiling and making some small, isolated changes.
This is the tracking ticket for the larger-scale reworking of the compiler internals for better performance: https://github.com/rust-lang/rust/issues/48547
I think there are a couple of major reasons why rustc is slow, and then a lot of ones which just boil down to "we haven't gotten to that yet."
One of them is that rustc was originally written as a self-hosting compiler while the language itself was still in flux. So for the early years, there was a lot of churn just due to keeping up with the language changes, and just implementing the language, not being able to really focus on performance and optimizations.
Once the language stabilized, the compiler was not really the best written piece of code in Rust because it had been written before some features existed, before people had figured out idiomatic ways to do things in Rust, and because it had been written with the assumption that it could just work with the AST and a high-level IR and translate that directly to LLVM and let LLVM handle all of the optimizations.
Shortly after the 1.0 release, a big project began to define and use "MIR", a mid-level IR between the high-level IR that is much closer to surface syntax and LLVM's low-level IR for generic optimizations and code generation. That project took a while to complete, but has finally been completed; that lays the base for new language features, improvements in existing language features, and being able to do better work on optimization; it has led to several major improvements in compiler performance that have already landed.
Another issue is, pretty similar to the C++ issue; Rust has generics, which offer many of the same compilation issues as C++ templates. They are constrained a bit more than C++ templates, and don't have the SFINAE philosophy, so I think it's possible for the compiler to be smarter with those, avoid monomorphization in some cases, and so on, but it is still likely that in heavily generic code, you may not be able to do much better than heavily templated C++ code.
There are also some issues that are just holdovers from the fact that language evolution, correctness, and ease of development was prioritized over compiler performance in the the initial versions; for instance, the de-sugaring of high-level constructs causes rustc to produce a lot of LLVM IR that LLVM winds up just optimizing away. Having rustc be smarter and produce less of this is a major optimizaiton goal.
A few major examples of improvements that are underway or being considered: sharing generic code between crates (if two crates in your dependency graph each instantiate Vec<i32>, you shouldn't have to compile that twice), sharing closures between generic instances (if there's a function that depends on some generic type T, but a closure in it that does not, there's no need to duplicate that closure for both instances of that function), doing some inlining in MIR (lots of high-level Rust features depend on inlining to produce "zero-cost abstractions", so doing that earlier in the process may reduce the amount of work that LLVM has to do).
And then there are improvements for allowing for better parallelization and incremental compilation, better link time optimization, and so on.
tl;dr: it will get faster, in a real sense, not micro, but in the end it will be constrained by language design to be closer to the C++ compiler end of the spectrum than the Go end of the spectrum
It would be nice to be able to use something like Keith Winstein (Stanford) et al's gg.[1] Sort of `make -j1000` for 10 cents on Lambda. Linux kernel cold compilation in under a minute.
[1] https://github.com/StanfordSNR/gg ; video of talk demo: https://www.youtube.com/watch?v=O9qqSZAny3I&t=55m15s ; some slides (page 24): http://www.serverlesscomputing.org/wosc2/presentations/s2-wo...
For Rust, though, the answer is obvious - the borrow checker. The language is designed around the idea of the compiler proving whether or not the code is safe and inserting appropriate destructors where they should be. I think it's expected that this will be slower than a language that just doesn't do that.
The NLL version is slower than today’s, at the moment, though.
The query parallelization work has been coming along. Also it was recently discovered that building LLVM with a recent clang and doing cross-language LTO speeds up builds by ~25%.
Practically every VM that tried to use LLVM as a JIT gave up as it was too slow. WebKit eventually abandoned their LLVM backend and wrote their own, B3.
JITs are of course a specialized use case, but there are also known issues with compilation speed in LLVM. For example, LLVM is single-threaded, so compiling a single very large compilation module can be much slower than it needs to be - and this is an issue hit by Rust due to how crates are designed. But you are also very correct that Rust is generating overly-complex LLVM IR for LLVM to compile, which is another problem there.
Also worth noting that while once clang beat gcc in compilation speed, as clang's optimizations have caught up to gcc the difference has vanished, and on many benchmarks today gcc compiles more quickly.
And it doesn't even seem to work effectively as a target for JIT. It can do the machine code emitting part of a JIT well, but that wasn't really the problem and it doesn't seem to work well at the real problem of optimising code for a high-level language. You need to optimise yourself, by writing basically your own compiler pipeline, before emitting LLVM.
See for example the Rubinius implementation of Ruby.
However, comparing compilation speed across languages and compiler implementations is a bit difficult.
(This is true for performance and optimisation is general: fast enough is good enough.)
Once it takes seconds, we can talk about "fast enough".
Is that minutes for every compile, or for an (infrequent) full compile?
With Go, on my laptop, 30 seconds is the time it takes to perform a complete, optimized build of a medium-sized application, all dependencies and the standard library from source.
It's always so... frustrating whenever I go from a Go project back to our large C/C++ mixed codebase...
https://dave.cheney.net/2016/11/19/go-1-8-toolchain-improvem...
"Clang" is a thing not a person, therefore it's appropriate to use et cetera, not et alii. The former is for things, the latter for people (usually limited to authors of academic papers).
Anyway, you could argue that the use of "et al." in English has a restriction that doesn't come from the original Latin. Many of the Google results I found do claim that there's a convention of using "etc." for things and "et al." for people, although that seems to conflict with the idea that "et al." primarily stands for "et alia". But they mostly don't claim that that convention is a hard rule. And I don't think it makes much sense to establish one, considering the original meaning.
[edit: reworded for clarity]
This is a genuine case of survivorship bias in action.
I guarantee you that there are people not using Clang or writing, let's say, C++ because of slow compile times. You just never hear about how much they hate using them, because those people no longer are using them.
Well, the Go standard compiler doesn't do much.
They might have some more comprehensive optimization passes eating extensive cycles, but even rustc debug builds are extremely slow in comparison to the Go standard compiler.
(One of my benchmarks for "Go has really made it" is when someone starts selling an optimized Go compiler that does the expensive optimizations. You could still ideally use the standard Go compiler to develop, but then you'd test and deploy with the slow optimized compiler.)
However, then we can draw on two facts to kill that this should be a significant part of the issue: The post states that for most projects, the majority of the compile time is spent in LLVM, not Rust. Second (my memory may fail me here, but this is easy to measure), for larger projects, Go also smoke clang/gcc, which do not have Rusts fancy features.
But, as I stated in my parent comment, these cross-language, cross-compiler comparisons are quite difficult to make. This is also why I attack comments talking about one compiler doing more "work" than another. It cannot be directly compared.
All we know for certain, is that rustc is far too slow for our liking.
Were I going to start the company to produce the compiler, the question of "why isn't gccgo about 2x faster than Go?" is one of the first I'd examine, though. I personally have zero idea what the answer is. Or, indeed, if it's even still true. However, I am active enough in the Go community that if good benchmarks were going around I'd expect to have seen them. (They'd probably make it to HN.)
I am frankly too lazy to set up gccgo for comparisons again, but if I recall correctly, it takes considerably longer to compile whilst providing similar quality output. Thus, I would ask the opposite question that you presented:
If gccgo isn't notably faster than gc, why is compilation so much slower?
If one compiler spends more time generating identical quality output with an identical input language, it is hard to argue against the fact that there are wasted cycles at play. It may of course partly be due to an immature Go frontend. Who knows.
As the OP pointed out, Rust debug builds are still much slower than Go builds, so lack of optimizations can't be a big part of the story. The simplicity of the language and the deliberate design of the compiler for speed seem to be the main factors.
I'm not claiming this is true by any means, but it wouldn't surprise me that much that merely the work to tell if a bit of Rust code in, say, Servo, is legal Rust in the presence of a rich type system and the borrow checker and all the other such things going on is more work than it would be to compile the roughly-equivalent Go module entirely. Compared to Rust, Go does not so much "cut corners" as cut entire dimensions out of the picture, and then cut some more corners for good measure.
Work is not just the optimization.
Having Rust track lifetimes and warn about ownership bugs, races, etc, is also productive work for the compiler -- and happens during the debug builds too.
I stated in my parent comment that cross-language/cross-compiler comparisons is tricky exactly because of the vast differences in both frontends (lifetime tracking, warning generation) and backends (code generation, optimization). So yes, I know there are different amounts of work related with different languages.
However, I believe the compile time differences significantly outweigh the compile time differences.
A, true, by the time it hits LLVM Rust's lifetime's analysis has already happened...
> However, I believe the compile time differences significantly outweigh the compile time differences.
I meant to say "the compile time differences are significantly greater than the language-independent differences".
I've edited my original comment to include the clarification.
[0] http://polly.llvm.org/docs/Performance.html#compile-time-imp...
That said, it's generally not seen as part of the main stack, which would be LLVM, Clang, compiler-rt, and maybe lld, libc++, and libc++abi. (The latter three being fully optional replacements where most people use the standard system components instead).
No problems, compiling all of the dependencies was fast, examples worked, and in literally about 2 minutes I had a neural net and was hacking on it.
This is without touching rust in months.
Sometimes it's just quicker to work with two separate working directories than switching branches in one.
When someone like ajross says rustc is really slow, how slow are we talking?
It really depends on your machine, as you ask above. And on your project. And if it’s a fresh build or a rebuild. And all sorts of stuff.
It’s slow enough that improving this is a priority for us.
Let's say, for example, you're on a regular rebuild cadence for your work. What makes the biggest difference for speed in such a scenario: CPU cores, CPU speed, DRAM speed, SSD latency, SSD throughput, something else?
It is interesting because it advances the state of the art: it makes safe "systems programming" possible, which no production ready language previously provided.
It is interesting because it is accessible to many of the people who frequent HN. It's community is similar enough to the community of HN readers that many HN readers can use Rust and not feel out of place. The concepts it introduces are not such a departure from the concepts familiar to many programmers that a huge amount of learning is required. (Compare to, say, more research oriented languages like Idris).
More specifically, I would say that it lands in the Goldilocks range: It's not so alien as to make it inaccessible, while being alien enough that you'll learn something from it.
I think you have just made Ada and several other languages disappear from the history.
If you start a new project using Rust there is a reasonable chance you'll be able to find libraries for common tasks and developers willing to work on it. You can make a case for it and not lose your job. With Ada, outside of a few niches that isn't going to be the case IMO.
It's been a long time since I looked at Ada, but I believe it's memory mgmt is more inline with modern C++ than Rust. So perhaps it's not truly comparable.
Which is irrelevant to the claim that no previous language provided safe environment for systems programming.
For some people, a good library selection is essential to being "production ready".
And then, Ada had a selection of libraries for a long time. You just conveniently forget about the paid ones, so you can call Ada "not production ready".
That said, I agree that it'd be moving the goal posts to omit paid libraries.
--
I do think we have slightly different ideas of "Systems Programming", or at least what production ready means for that type of language. I think it boils down to the fact that people do a lot more in "Systems Programming" languages than just OS or implementing and providing interfaces. Specifically, people do it for performance and control, which I don't necessarily excludes the use of libraries.
I think the contention here is that people are thinking of production ready for these types of languages in terms of general use, not just the niche you describe. In retrospect, I do think it was a poor way to word it.
I guess I'd probably amend the original comment to say something like "Rust is the first production ready systems programming language that can comfortably (after a learning curve) be used at higher levels".
And then in both language, you can use unsafe when you need more power. So Rust might (and I say might, because I have no clear proof of that yet) be a tad more powerful in the safe subset, but it's not in a different class.
Where did you get your information from?
The memory safety concepts are novel and unique to Rust but enough has been talked about them already.
Additionally, it brings a lot of features from "advanced" programming languages (ML, Haskell) to a more mainstream language. Sum types, pattern matching, powerful macros. None of these are new or unique, but are executed particularly well in Rust.
In addition to the new features and concepts, the Rust compiler and ecosystem are well engineered and have a good amount of support.
It's the first "new" programming language in a decades worth getting excited about. Ok, maybe I was a bit excited about Julia too, but their execution is not as great as Rust's (and they have made some questionable design decisions).
Could you expound on this? The code I currently write and expect to write in the near-to-mid-term is numerical for engineering purposes. I have the luxury of being able to write greenfield projects, at least for now. My experience with OCaml has inculcated a deep appreciation for powerful type systems and functional styles, so Python, despite its awesome ecosystem, is not particularly of interest. Julia appears to be the natural choice in this space for new development, but I've been a little leery after reading about Dan Luu's experience a couple of years ago: http://danluu.com/julialang/
I'm interested in reasoned criticisms of Julia as it stands now, and what modern practical alternatives people have found.
Rust shows some promise of being that language, and so we hold out hope and enjoy reading about it.
I like rust for a few reasons.
* I really like Rust developers - I've met many, talked with many, and we get along and they've helped me through many technical problems, both big and small.
* I found rust extremely easy to get started with. I knew I'd have to learn the language, I did not also want to learn build tooling, package management, etc. These barriers exist in many other languages, it's frustrating because I just want to focus on programming. Cargo is excellent and I am a huge fan of how rust manages crates.
* I find it very easy to express what I want in rust, to reason about the code, etc. I know when a function might error, I know when I need to handle some unforseen case, I can express how a variable is used, when it is usable, whether it can be mutated. I can cut down on boilerplate with generics and macros.
The last point is what keeps me coming back. I find rust extremely easy to write, because it makes the things I care about easy to express.
I wouldn't use it as an alternative to garbage collected languages since garbage collection is just simpler overall, but I would consider using it as a safer alternative to C or C++.
I do not disagree, but I think one could argue to the contrary as well.
Since most garbage collected languages do not guarantee that objects are actually (timely) garbage collected, you cannot tie the lifetime of other resources (file descriptors, sockets, locks) to object lifetimes. So, the burden is on the programmer to ensure that resources are correctly finalized. Whereas in RAII languages you can properly tie all finalization to object lifetimes.
Note: I am not arguing that GC-ed languages cannot do RAII. AFAIR D has a GC and supports RAII.
This is not as good as the Rust model, but it's not awful, either.
Where things get complicated is when you don't have a clear scope to tie lifetime to and so you can't use these, but then, that applies to RAII too.
I'm not saying C++ doesn't have a bit of an advantage here, but I think it often gets oversold as "C++ has RAII and other languages have nothing even remotely resembling it", which isn't true.
In practice this isn't a problem that I encounter in GC'd languages anywhere near often enough to justify even a slight preference for a "true RAII" language.
I would say that in principle they work very differently ;).
They work superficially in the same way in that if you tie a particular object to the current scope in a RAII language, the cleanup happens at the same point as defer, with, try-with-resource, etc. would. However, there are cases where you really want cleanup to be tied to the object's lifetime.
For example, I have a Tensorflow binding for Go. However, I cannot pass Go-allocated memory (e.g. memory allocated to a slice), because Go does not allocate slice memory on 32-byte boundaries (using Go memory would cause an allocation + memcpy in Tensorflow to align the memory). So, you allocate memory in C-land and have Go structs that wrap your pointers. However, now cleanup becomes interesting. You do not want to rely on finalizers, since they are not guaranteed to run. However, using a Close method is also an annoyance. For tensors that live for the duration of a graph run, it is fine (you can use defer), but other tensors live longer and are reused between runs, shared by models, etc. It becomes unclear pretty quickly who is responsible for closing the model.
I also use a Tensorflow binding for Rust, which is drastically more convenient in this respect. Since ownership is clear, the lifetime of a tensor is bound to the scope or object that owns it. If the owner is dropped, the tensor is also dropped. If you need to share a tensor, you make an Rc/Arc the owner.
People tend to then get annoyed at me for expressing this opinion, but this sort of thing is the reason why. Go is really only merely adequate at interfacing with libraries in other languages [1] which scientific programming does a lot of, and Go has a type system that seems almost precisely tuned to get in your way if you try to program mathematical code in a typed manner but at the same time isn't so weak that you can pull something like a NumPy where at the Python level everything is just untyped so as long as you assemble it correctly up there, the C level can work it all out. Nor can you practically program in that manner, because while you can slather interface{} everywhere, you can't make it convenient to work with like a dynamically-typed language. I think Go is approaching maximally pessimal for scientific-type programming, personally.
I should clarify that when I say Go has "something like" RAII, I do mean pure-Go code only. And by no means is "defer" perfect. (I'm definitely in the camp that it should have been block scoped, not function scoped, and the performance hit can be quite annoying.) It's just that, as I said, it's not like the choice is "either RAII or you're in some manual-management only horrorland"... lots of languages have block-scoped constructs (not just Go) that can be used to 80/20 RAII. That last 20 may be important in some cases, but it's quite often a great deal less important than the 80.
(This post is brought to you by your friendly local "HN poster who has been accused of being unreasonably positive about Go".)
[1] Pretty much every modern language claims to have "great" interfacing with C, despite IMHO wild variances in difficulty. Go is "adequate" because it's not too difficult to simply call a C function, and with not much labor you can get binary-level-compatible structs between the two, which is a nice advantage over Python or Perl or something. But the semantic mismatch is pretty rough around memory management and threading model, and that manifests in slowness in the calls in addition to general semantic mismatch.
For me, the most promising compiled and statically-typed language for scientific computing is Nim.
Disclaimer: I am the author of a Numpy/Torch/Tensorflow-like library written from scratch in pure Nim, the look and feel is pretty similar to Python Pytorch + Keras for neural networks: https://github.com/mratsim/Arraymancer
That's a good point. Go was my camping ground while Rust was still breaking every month and I didn't want to go back to C++. Although I don't use Go anymore, I have come to appreciate its simplicity and in a lot of scenarios I would definitely recommend it.
I did similar thing with C# more than once. Not with TensorFlow, but with my only C++ libraries that also used SIMD and therefore required aligned memory buffers.
It worked just fine. There’s IDisposable for deterministic cleanup, and finalizers as a safety net. Unmanaged interop, i.e. [DllImport], is supported on all platforms, e.g. on Linux it imports from *.so libraries.
Sure, but Rust does help here specifically: resources can safely be moved into child or parent scopes.
I would, too. But there’re problems for which C or C++ is just faster because of language/compiler extensions like SIMD intrinsic or OpenMP.
Also Rust can’t be used for GPU code; strictly speaking C can’t either, but practically CUDA, C++ AMP, OpenCL are very close to C and/or C++.
Also if you have to deal with lots of pointer-based data structures (trees, graphs, etc)., and it’s not just on the lowest level you can abstract away behind a safe API, I don’t think Rust is safer. To get performance comparable to C++, it’s necessary to use unsafe rust for raw pointers, and IMO modern C++ is safer that unsafe Rust.
In Rust, the borrow check is still active even in unsafe blocks. In C++, however, the complex rules around when destructors are called, combined with references, are a constant use-after-free footgun that you can never eliminate.
Rust projects that use unsafe code are empirically safer than C++ projects. See, for example, the Servo style system as used in Firefox.
When you deal with pointer-based structures (trees, graphs, etc.) you’ll use raw pointers for them. Raw pointers are not borrow checked, and the complete list of pointer-related potential bugs applies to unsafe Rust, you can use after free, double free, mess with pointer arithmetic so you’re out of bounds, etc.
> In C++, however, the complex rules around when destructors are called, combined with references, are a constant use-after-free footgun that you can never eliminate.
C++ ecosystem has lots of stuff that help writing correct code despite unsafe language. Debug builds use special version of heap that fills freed memory with a magic number, this help to catch most UAF bugs very early in the development. There’re runtime tools like valgrind and asan. There’re static analysis tools like PVS studio, clang static analyzer, and coverity.
Unsafe Rust has none of them. Not even a debug heap.
> Rust projects that use unsafe code are empirically safer than C++ projects.
Survival bias: people who need raw pointers and other performance-related features like SIMD and manual RAM layout don’t pick Rust for their projects.
Sure it does! I've used most of those features in Rust, except for Address Sanitizer. But even ASan is available now: https://github.com/rust-lang/rust/pull/38699
I remember spending hours using Valgrind on Rust code back in 2011 to track down codegen problems in the compiler :)
I also think many of your points over generalize. Finite state machines are graphs for example, but I've never needed to use raw pointers to achieve the performance required. Similarly, I definitely need SIMD, and I use Rust for that.
I can, and I do. The trick is to use some other safer language for less performance critical higher level code. C# often works well for me but there’re others, e.g. in data science community Python is quite popular for that role.
> I've never needed to use raw pointers to achieve the performance required.
Very likely, we work on different kinds of projects. One of my ongoing project is a CAD/CAM app for Windows, and I have implemented quite a lot of pointer-based stuff to optimize the performance. Technically they are mostly graphs, logically they’re multi-level hierarchical containers, LRU caches, quad-edge structures, scene trees, external indices, etc.
P.S. Why do you think Mozilla doesn’t use Rust for their DOM tree implementation, and instead relies on the JavaScript runtime and it’s GC? Don’t you think sometimes other people might also want DOM-like structures in their apps, be it for a web page, XML document, objects hierarchy in 3D space, or any other kind of data?
> Very likely, we work on different kinds of projects.
I work heavily with finite automata. Similar libraries written in C or C++ use pointer based stuff quite heavily, but none of it has so far turned out to be necessary in Rust while also achieving comparable performance.
This continues to miss the point that Rust permits building safe abstractions over unsafe internals. Even if you need to use raw pointers in Rust, you're not only no worse off than you are in C++, but you can actually encapsulate that use in a way that is guaranteed by the type system without resorting to using a completely different programming language.
> Why do you think Mozilla doesn’t use Rust for their DOM tree implementation
I'm not involved with that project, so I wouldn't know, and I wouldn't speculate. You shouldn't either.
I’m not pretending that. Just two points.
1. If you really need unsafe code, because raw pointers, or other reasons, C++ is safer than unsafe Rust.
2. If your project has two distinct parts, unsafe lower level, and safer higher level, you don’t need to use C++ for both. Real world software use multiple languages in the same project for decades already, e.g. QuakeC was developed in 1996.
> a regular expression library might use unsafe internally
If a library wants to do that, it probably means that safe language is too slow :-) https://github.com/dotnet/corefx/tree/master/src/System.Text...
> I work heavily with finite automata.
I’m not saying Rust is useless. There are projects where it shines, and where I’d probably picked it myself. Like a web browser CSS engine, or your finite automata, or many other things.
I’m saying that there’re large problem areas out there for which, due to various reasons, other languages are still way better than Rust. I do realize Rust evolves fast; eventually these areas might shrink or disappear. But in its current state, my opinion is the applicability is limited to very narrow areas: no bare metal, no SIMD, no GPU APIs, very limited asynchronous IO, limited embedded options, very limited numeric libraries, no GPGPU…
I’ve only listed the areas where I have recently (last couple of years) developed substantial amount of code in any other language.
It just happened that I didn’t work on either finite automata, nor browser CSS engines.
I don't agree. I've told you why. I find speaking with you very frustrating, and it's not clear to me that you've actually understood my point unfortunately. :-/
> If a library wants to do that, it probably means that safe language is too slow
That doesn't make any sense.
Yeah, same here. You’re insinuating things I didn’t said nor meant.
> it's not clear to me that you've actually understood my point
I disagree with your points, and I've told you why.
Nothing prevents you from writing a DOM tree in Rust, but when in comes to the Web DOM you need to interact with JavaScript and with the JS GC no matter what you do. How is DOM managed in other browsers written in C++ ? I would be surprised if it wasn't also handled by the JavaScript runtime.
But even if it’ll become available in 2 months, it’ll take some time to develop Rust libraries on top of them.
E.g. in various performance-critical C++ projects I have used https://github.com/Microsoft/DirectXMath and https://eigen.tuxfamily.org
Both are quite large projects with many man-years spent to develop, they saved quite a lot of my development budget.
Of course, more libraries are needed as well, but there’s already some higher level stuff. See https://github.com/AdamNiederer/faster for example. And const generics, coming to nightly near the end of the year, will be another step up. It's true overall that numeric stuff is a weakness, but we'll get there!
I wouldn't say zero-overhead. I would say O(1) overhead in the best case, and equivalent to C-style manual memory management in the typical (which again isn't zero-overhead and isn't even necessarily always O(1) overhead with heavy allocation and deallocation).
But, yes, it isn't GC'd with the attendant challenges GC poses (or benefits it brings).
Many papers exist but often have conflicting results, or weird methodologies, or extreme artificiality. It's really expensive/ hard to generate good research on this topic that controls for the big variables - you basically need a company that's willing to throw away at on of developer time to build two versions of the same product in both languages, then a metric for measuring success, a way to ensure you take dev skill into account, etc.
So instead it seems a lot more worthwhile to not bother looking for objective research as it won't be found.
I think a better approach is to just use what we have - sense and anecdotes.
From a sensibility standpoint, let's take a step back from rust.
Do we expect fewer memory safety vulnerabilities in Java vs C? Probably yes - Java is a memory safe language with few escape hatches.
So what about Rust vs C? It's a bit less clear as rust has a more oft used escape hatch, but I think we can reason that it's likely to have fewer memory safety bugs.
Would we expect fewer bugs in general? That's much harder to discuss with that approach.
You can look at testaments from Servo devs, and other companies using Rust ( https://www.rust-lang.org/en-US/whitepapers.html ) and their experiences.
I know that the parent post used multi-threading as an argument in favor of Rust. But there are many ways to blow of your foot in single threaded C++. For example, consider this single-threaded code:
std::vector<size_t> v;
v.push_back(1);
size_t *first = &v[0];
std::cout << *first << std::endl;
// The STL vector implementation will probably
// reallocate the backing array at least once.
for (size_t i = 2; i < 1000; i++)
v.push_back(i);
std::cout << *first << std::endl;
You wouldn't be able to introduce the same error in Rust. Once you borrow the an element of a Vec immutably, you cannot create mutable borrows during the lifetime of any immutable borrows. So the compiler would reject pushing new elements.However, the mental cost a Rust programmer pays for that trade (due to borrow checker) seems too high for me
The thing is that most of the errors that the borrow checker will point out are potential programming errors in C or C++ as well. However, current C/C++ compilers will put most of the responsibility completely on you (plus tools like Valgrind). So, to me C++ has at least the same cognitive load if you want to program responsibly.
A good example of the challenge is that there's unsafe inner code in rusts own container code. If you try to build your own custom containers (or things like arenas) you'll likely work with unsafe code... Or rely on rusts container code (not a bad thing of course).
My feeling is that "C to Rust" is close to "Java to Haskell" more than anything else. The extra layers are important enough that they need a new thought process. (I say this as a proponent of rust)
> You really need to lay your code in away that will satisfy the checkers.
There are some patterns that are common but problematic but are being addressed by non-lexical lifetimes. Other than that it's almost "program in C++, then fix errors".
But that becomes a second nature after a while. I think all the edge cases that still exist make it more problematic. E.g. I was writing some SIMD code yesterday with a loop somewhat like this (simplified):
let mut v = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let mut s: &mut [f32] = &mut v;
while s.len() >= 2 {
println!("{:?}", s);
s = &mut s[2..];
}
Playground: https://play.rust-lang.org/?gist=104348b0edb96ca0f68d701373e...It makes you scratch your head for a bit.
A good example of the challenge is that there's unsafe inner code in rusts own container code.
I am pretty proud that one of my first small Rust projects was a randomized ternary trie. Apparently, it does not use unsafe anywhere.
https://github.com/danieldk/rtrie/blob/master/src/ternary.rs
Not that I am proud of the code ;).
But in all seriousness, unsafe in container code is somewhat expected, because at some point you need to wrap raw memory (as e.g. in Vec). I think quite a bit of, but probably not all, unsafe use in containers such as Vec comes from that.
i remember reading about the stylo (multithreaded css styling) engine, something that has been tried times and times again to implement it in C++ without success: too complicated. sooner or later you'll get too-hard concurrency bugs and the project would be abandoned. and it wasn't just mozilla with firefox, google also failed to implement concurrent styling with chrome.
the blog post stated that this time they were successful _only because of rust_. the mental overhead of managing the borrow checker is far lower than the mental overhead of dealing with concurrency issues in a complex high performance project.
They thought it worthwhile to invent a new safer language for massive parallelism and security, rather than to attempt it in C++.
Also coming from Java, the Rust experience is not that painful. It's like learning a completely foreign language.
void Scheduler::schedule(const Job& job, Result* result);
void Scheduler::wait();
The consumer of this API in C++ has an easy job. They can create an array of results and iteratively call schedule(jobs[i], &results[i]) for each job, and then call wait(). A similar API in Rust would be a giant pain in the neck to use, because there's no simple way to collect the results. You'd likely want to scrap this API entirely and have Scheduler expose some kind of queue or something.
My point is that these extra constraints are not just extra typing locally to specify how you're using lifetimes. You'll likely have to rethink the way you write APIs and whole programs in Rust because of the ways it restricts mutability. Which is OK, it's a trade-off, all I'm saying is the safety doesn't come for free.
The mutability thing isn't a problem. You can allocate a vector of results and jobs, and then have a mutable iterator over them, zip them together and then iterate over that. There's no problem passing mutable references to each of the elements.
The right way to implement the API above is for schedule() to take Arc<Mutex<Result>>, and explicitly share mutable ownership over each individual result between some unspecified asynchronous execution mechanism and the caller of schedule(). This makes the calling code significantly more complicated as it needs to incur the overhead of atomic reference counting and a mutex per result, and must try to lock each result before using it, even if it knows there are no other owners of the result due to wait() returning.
https://play.rust-lang.org/?gist=1324a919ca5d0e2f16e445364a7...
Rust also offers some other methods to allow multiple mutable references to containers: .split_mut() comes to mind – that splits a mutable slice into two mutable slices that don't overlap. In the future I'd like to see in the stdlib "container adapters" that take in a normal container and provide an interface to it that allows taking multiple mutable references while ensuring the soundness using dynamic checks (it can keep an array of the pointers or indexes that are borrowed out, and check against that). I created such an interface as an experiment last year: https://github.com/golddranks/multi_mut
In C++ land, you'd have to use TSan thread sanitizer and a lot of other tooling (ASan, Valgrind, etc) to achieve even close to the same level of confidence. Not only is this a runtime check but all that tooling has a fairly heavy mental cost too.
Better to be charitable, I think. Doing so costs us nothing.
Rust is a fairly conservative language. It doesn't really do anything novel, it mostly just synthesizes patterns that have existed for years. Hopefully Rust truly is mediocre (at least in hindsight). That would mean the things it is attempting to replace are seen for how primitive they truly are.
This is so well said that I agree 100%, and couldn't say it any better.