Why Rust fails hard at scientific computing
andre-ratsimbazafy.com
andre-ratsimbazafy.com
I went and Google'd "rust linear algebra library" and immediately found https://nalgebra.org which can represent such matrices just fine, and even integrates with LAPACK. Did the author do this most cursory of research before springing to write their rant? This is like saying "Python fails hard at scientific computing" because the default list type isn't an efficient basis for representing matrices, while entirely ignoring NumPy.
Also, those are memory slices, not arrays: they aren't pointers to heap-allocated data. So [[f32; 9]; 5] corresponds to a contiguous block of memory representing a 5x9 matrix of 32-bit floats. It has the same memory structure as [f32; 45], but you can index it as eg. my_matrix[3][2] to access the element in the 2nd column of the 3rd row. Again, I guess it's much easier to bash something than try to understand it.
The author didn't want to use/leverage/whatever some ready made package. He wanted to write his own.
Python does suck for scientific computing at that level as well -- Numpy and Scipy are huge wrappers for C/Fortran etc code.
From the author: You can work around it by using a Vec (arbitrary sized sequence/list) but then your matrix is allocated on the heap not the stack, meaning slower operations. Plus that means you cannot use Rust wonderful type system to check that you multiply matrices with compatible dimensions, say a 2×2 matrix with a 2×1 matrix, without jumping through hoops.
So he thought of that, then decided not to go that way, even though that's how it's done in C/Fortran underneath every single linear algebra library in the world, and his conclusion is that rust sucks?
I'm finding it a little hard to believe that there are real-world linear algebra problems where allocation is the bottleneck and doing it all on the stack is the answer.
If you're doing linear algebra on big matrices, putting them on the stack is most likely madness. It sounds like the author is interested in Rust for the 'expressive' type system enforcing logical invariants about his matrices, which isn't why Rust built their expressive type system.
I guess this also holds for graph-problems, where graphs are possibly cyclic structures (difficult memory management), and/or stored as adjacency matrices.
PS: Is there a set of benchmark problems that can be used to validate a language design?
Just because language X can wrap a LAPACK/BLAS library doesn't automatically make it fit for the purpose of implementing one.
Look at how the nalgebra represents an n-dimensional array of known dimensionality (through both the type system and in memory) and you will understand.
Once there is, say, a uBLAS equivalent in rust that is easy to use and understand the implementation of, I will consider it worthy.
More generally, the 1-32 issue should be solved when RFC 2000 is implemented [0]. In the meantime there's generic-array [1], built on top of typenum [2], which you could think of as a sort of polyfill.
[0] https://github.com/rust-lang/rfcs/blob/master/text/2000-cons...
fn main() {
let my_str: Rc<RefCell<Box<MyStruct1>>> =
Rc::new(RefCell::new(Box::new(MyStruct1)));
my_trait_fn(Rc::new(RefCell::new(Box::new((**my_str.borrow()).clone()))));
my_str.borrow().my_fn();
}
I come from a Python/Java world and I remember when I first delved into embedded MCU programming in C/C++, it was a cognitive overload and a half. I think we need to take a step back and while I understand the differences between high level languages (Java/Python) and C/C++ having the burden to allow for low-level programming; readability aspects of code often gets the lowest attention.If we, humans, spend so much time writing/parsing code through our vision system, programming languages need to take this aspect seriously and optimize the syntax over readability than expressivity.
I recall Kernighan's "The Elements of Programming Style" where he talks about making the code more understandable than "clever". The syntax of a programming language directly facilitates (or impedes) this idea.
Edit - Everyone seems to keep pointing out it is a contrived example. Yes, I agree but the syntax has nothing to do with making the contrived example even more difficult to read?
Suffice it to say that you should probably not judge languages based on examples written by people who don't understand the language.
Heavy use of a construction like Rc<RefCell<Box<_>>> is often a sign that someone is trying to replicate something like c++'s std::shared_ptr<_> in rust rather than write code in a manner idiomatic to rust. Also, given that the article's second point results from a confusion between a memory slice and an array, it is difficult to see this as an informed discussion of rust.
If something in a language feels badly wrong, it's often a good idea to ask experienced people, or even the language's authors.
It's also very unclear why they created an entire second Rc rather than just cloning the first one, as that's the point of Rc.
Compare:
fn my_trait_fn(t: &MyTrait) {
t.trait_func();
}
fn main() {
let my_str = Rc::new(RefCell::new(MyStruct1));
my_trait_fn(&*my_str.borrow());
my_str.borrow().my_fn();
} let my_str = Rc::new(RefCell::new(Box::new(MyStruct1)));
Yep, even Rust has better syntax ;). The call is a little more gnarly. Keeping in mind that I don't really know rust, my understanding is that this is roughly the equivalent of trying to cast, in C++, shared_ptr<scoped_ptr<MyStruct1>> to shared_ptr<scoped_ptr<MyTrait>>. It's doable, but why the heck would you do it? The C++ equivalent might be: auto my_str = shared_ptr<unique_ptr<MyStruct1>>(new unique_ptr<MyStruct1>(new MyStruct1));
my_trait_fn(shared_ptr<unique_ptr<MyTrait>>(new unique_ptr<MyTrait>(new MyStruct1(**my_str))));
Which isn't much prettier. Why not instead: fn my_trait_fn_2(t: Rc<RefCell<MyTrait>>) {
t.borrow_mut().trait_func();
}
fn main() {
let my_str2 = Rc::new(RefCell::new(MyStruct1));
my_trait_fn_2(my_str2.clone());
my_str2.borrow().my_fn();
}
EDIT: Added a C++ example which actually compiles. EDIT x2: Fixed a double free. int* foo; // This is a pointer. Understood.
int *foo; // How is this allowed!???? Confusing!
*foo = bar; // Now you're deferencing. WTF!
In the second line, int foo confuses the hell out of me when I am scanning through pages of code. All of this mess could be solved by doing the pointer symbol after the variable name: int foo*; // This is a pointerIt's more clear with this example, which declares two variables at the same time:
int *foo, bar;
This declares foo as a pointer to int, and bar as an int. The following, written in the style you suggest, do the same thing but can be confusing, since a casual reader might think that this declares both foo and bar to be pointers to int: int* foo, bar;
Someone else commented that C's declarations are designed to look the same as how they are used. This is true, and explains why arrays are declared the way they are, where the brackets come after the variable name: int foo[10]Likewise for arrays. You declare it `int foo[]` and you use it like `foo[i]`.
I have looked quickly at Crystal and I liked it, really did. But it's too late for me now, I am in love with Elixir and already using it professionally, quite successfully too. Now that's a language with 90% WTF-free syntax (admittedly some of the pattern matching idioms and a few constructs can be confusing at times; very interested to see the builtin code formatter coming in version 1.6 though).
Well, this is the generic curly-braces-and-semicolons style. If you want to see nice clean yet type-safe syntax, just take a look at OCaml or Haskell.
A large portion of readability comes from familiarity. I find most Haskell code fairly easy to read—or at least, approximately as simple or complex syntactically as it is semantically. Other mainstream languages tend to be either much more complex syntactically than what they do semantically (“verbose”) or much simpler (“magic”).
Beautiful. Absolutely beautiful.
Besides conflating memory-management with syntax, this could also be made more readable by writing idiomatic Rust: use type inference in your `let` statement, fix your indentation on the last line, etc.
Don't you think regardless of familiarity, Rust doesn't quite have a beautiful syntax? I am following this guide and it's like the visual equivalent of nails on a chalkboard. Perhaps it's subjective, what do you think?
I do agree though that Rust, like C++ tends to hide the logic in implementation detail (pointer ownership, memory management code etc sometimes outweighs the actual business logic)
Obviously these things aren't in Ruby or C# because it's implicit.
But this is what systems programming is: you tell a computer both what to do, and exactly how to do it.
I'd love for a systems programming language to look like Ruby or Haskell but I can't say I think it's possible.
I once imagined a language should have two tiers where you can write the bulk (slow) of your code in a simpler gc'd language and the core parts in a lower level subset of the language. Today I do that with C# calling C for example, but the boundary too wide. Just having the same build system and package manager for example would be fantastic.
The post can be summarized as a critique of a programming language that did not meet the author's desires, but sections of the critique is (1) dislike of the syntax, (2) a specific missing feature, and (3) a very specific feature which doesn't exist in any of the popular scientific computing languages. It's fine to talk about not liking a tool, but it doesn't merit "fails hard".
Looking outside popular scientific computing languages, many Algol derived languages support it.
[1] .NET languages are used in scientific computing with packages like Alea GPU and IMSL.
Hell, Rust doesn't even have stable bindings around BLAS/LAPACK/LINPACK and friends which are the lifeblood of every language capable of scientific computing.
Give it time. Scientific computing was never the primary thrust for Rust, and other more pertinent domains have yet to reach complete maturity.[0]
Conversely, you'd be a damn fool to try and write Firefox in Julia.
When someone brings this up the usual excuses are trotted out:
- "It can be done with templates".
- "You can have an array of arrays".
Then there's bikeshedding. Some people want to be able to extract an arbitrary subarray slice from an N-dimensional array and treat that like a regular array. This is possible but complicated, and if implemented slows down the simple operations. Then there are people who want N-dimensional arrays where N is determined at run time. (This now includes the machine learning people.) Endless arguments between these groups resulted in nothing being done in Go. I haven't followed that discussion for Rust.
The big advantage of doing this in the language is that there's a lot of hardware support for doing number-crunching fast on dense arrays, and you want the compiler to be using it effectively. Matrix multiply on arrays of arrays is much slower than on a dense array where the compiler knows the spacing between the rows at compile time.
At least have arrays as good as FORTRAN. This is why some heavy number-crunching work is still done in FORTRAN.
I am wondering what your point is. There are excellent and performant C++ template-based libraries for tensors (Eigen, Armadillo, etc.). In Rust I have used ndarray a bit for linear algebra and it is quite good [1].
If you have parametric polymorphism and operator overloading, whet is the problem of doing it in a library?
The big advantage of doing this in the language is that there's a lot of hardware support for doing number-crunching fast on dense arrays, and you want the compiler to be using it effectively.
Last time I checked Intel MKL (and wrappers like Armadillo) was still the fastest for doing number crunching of dense arrays on CPUs.
[1] The primary problem I ran into was that some common expressions were not backed by BLAS yet.
Yes. This is one reason why I simply didn't care to convert research code I was using for my research from the decade(s) old Fortran into C or C++, when I was in academia.
I've been writing some machine learning products with Rust for a while now and I have to say I quickly abandoned using Rust for the computational components. The best way to use Rust is as it was meant to be: at the systems level, in this specific case for data and infrastructure code. Computation should be done using a language more suitable (e.g. Fortran) as one part of the pipeline. Rust is fantastic for writing pipelines that leverage other libraries for computation in this regard: the relatively strong static type system and easy error handling make it quite nice to work with.
One dimensional array + index multiplication doesn't make it clear to the compiler that the rows are disjoint. So the compiler can't assume there's no aliasing. It also probably can't strength-reduce the multiply to an add when advancing through an array in the "wrong" direction. FORTRAN compilers have been doing subscripting optimizations like that since 1956.
After those optimizations, it may be possible to use some of the SSE instructions on x86.
I can agree that however you choose to represent a multi-dimensional array in RAM, giving the compiler some idea about them, or 2D matrices at least, could lead to some special optimizations.
If I wanna add this to a language, what I need to consider to be as FORTRAN?
Do you have some links to these discussions?
If I have to write low level code, I might just stick with C or C++ - the devils I know rather than trying to tame a new incarnation of the devil that is Rust. The complexity and learning curve costs of Rust far outweigh the benefits at this point.
Edit: If I sounded like I am giving up on Rust above, that's not yet the case. I am currently enjoying challenging myself to learn something complex and to that effect, Rust is holding my interest.
Although I suppose I should go look at the 2nd ed draft when I need to look into more theory - reading the refs and borrowing section from 2nd ed right now :)
Initially I was referring to the fields on the struct, but ended up passing them around as parameters.
You have a referrence to this kind of problem on the NLL proposal.
But unless I missed it, the books don't talk much about this.
I haven't really used Gtk-rs enough to say more than that, really.
On modern C++ you can easily use this approach with std::enable_shared_from_this.
This doesn't seem right at all... Does anyone know more? Maybe they meant slow to allocate / deallocate? The author's problem appears to require you to generate a lot of temporary data and then throw it away. They might benefit from just writing their own pool allocator, so they don't have to wait for heap allocates (assuming that's their problem).
- Heap memory may not be reused as cleanly as stack memory, which means lower cache efficiency. Your suggestion of a pool allocator should take care of this.
- Runtime code for bounds checking needs to take the array size from the object even if it's constant. I recall there were XXX_unsafe() versions of accessors, which should not do any bounds checking, but the code is going to look ugly.
- Having to go through an indirection from the object to the memory buffer. Here you are at the optimizer's mercy.
So to avoid overly complex / ugly / brittle code where you need high performance, you are pretty much forced to write your own matrix types from the ground up, which is one of the concerns of the OP. I don't believe this is a terribly bad thing, and if the existing crates for this are not great yet, it would seem to me that it's just not been a focus for the Rust community, not an insurmountable problem.
It's not even a clear tradeoff for large arrays, because using value types on the stack implies lots of copies -- when you have a large block of data you want to refer to it by reference.
Also, stack size is finite and relatively small -- 2MB-ish default per thread on Linux, or is it 8MB? -- so placing large matrices there will work until it doesn't. Heap allocation has all of virtual memory at its disposal.
> 2. Arrays in Rust are a second-class citizens, actually I think they don’t even have their visas. I hear them laughing at me when I try to use them. You can’t even clone them:
RFC 2133[1] adds Clone to Arrays (and Tuples), it has been merged and implemented[2].
> 3. Rust is still “discussing” integer as generic type parameter (since 2015), meaning a matrix type Matrix[M, N, float] will not exist before a long long time. The following github discussions are quite the read:
It links to a bunch of stuff, but fails to recognize that this also has a merged RFC [3] and implementation is actively worked on[2].
[1] https://github.com/rust-lang/rfcs/pull/2133
[2] https://github.com/rust-lang/rust/pull/43690
The article is dated April 1st, your links on const generics are from later.
Julia has some extreme magic that it can do to make your life easy. Crystal will attract the Ruby-like crowd of people who will create very neat and effective libraries.
Edit: Shameless plug. My book, Nim in Action, is 50% off today with code HalloweenPB50 (From Manning). Perfect opportunity to grab a copy and learn Nim :) https://book.picheta.me/
There's a lot of really nice "magic" in Julia. I implemented an experimental numerical system (a binary representation of real numbers) and after generating the +-x/ operations, I had complex numbers, Fourier transforms, and lu decomposition for free.
With the macro system I then wrote a library that let's me generate verilog code and built a hardware implementation of the number system and deployed comprehensive tests cross correlating the results of all operations across native Julia, hardware simulation in Julia,and verilog version transpiled through verilator.
I wanted to play around with Mary Wootters' discoveries about Reed Solomon encoding, so getting working Reed Solomon code over arbitrary galois fields was as simple as building the type rules and basic +-x/ and then again using matrix solving to solve erasures (in this case you can tell the type system there is no order to the type so it doesn't do partial pivoting). Then I needed to do some brute force searching of a large parameter space. The core program for this search is about 25 lines of code in the main loop, which reads like simple math statements out of a well written text, and the backing galois field and Reed Solomon libraries are about 100 and 50 lines, respectively.
Why? Because Rust's memory management doesn't work well with cyclic structures, and closures are such structures. Closures have shown to be very useful for interactive code (see Javascript).
Someone please prove me wrong. (I'm wondering how Firefox deals with this).
The biggest problem with UIs is that they tend to be based on inheritance, which Rust doesn't have. It's not insurmountable; there are people who have written macros to make this kind of code more concise, or alternative UI libraries that don't rely on inheritance.
I suppose they only support read-only free variables then?
Closures are basically syntax sugar for a struct that represents the environment, and a function pointer which takes that struct as one of its parameters. So you can do anything that you can do with that setup, aka, anything :)
https://doc.rust-lang.org/book/second-edition/ch13-01-closur...
let mut sum = 0;
(0..10).for_each(|i| sum += i);
println!("{}", sum); // 45
Having cyclic values and/or mutation of shared values requires things like reference counting and cells, but this is what designed-for-UI languages like Swift are doing, just with more syntactic sugar (and, of course, the lack of it in Rust is still annoying for this sort of thing, but it isn't a fundamental blocker, and is less problematic than lack of inheritance, as steveklabnik says).I'm curious if the author has tried using macros, which would require him writing the code only once.
Most is done in R, Matlab, Python, and Julia and for good reason. Most people doing scientific computing are not coders that can do the work at a higher level at a productive level.
What do you mean?