I’d like to compare the use of Julia to Rust for an application in mind that involves a lot of matrix operations and parallelization. I wanted to get informed opinions from the Julia community. Have people here used Rust? What are your thoughts on the language versus Julia? Are there scenarios you would use Rust? For me the effort required to develop in Rust in order to avoid the garbage collector seemed very large, given that Julia is a prime position to serve my use case.
Specifically, in a scenario where I want to build a shared library that I can then use to build Julia/Python/Java bindings to, I am trying to identify whether Julia or Rust serve the purpose better.
I had originally created a discussion topic over here:
I have only a little experience with rust, more with julia.
Julia pros
Better ecosystem for scientific computing
Multiple dispatch + duck typing allow for very composable code. There are good chances, your matrix library magically work on SomeonesExoticMatrix{SomeoneElsesExoticNumber} without any effort.
Very interactive, REPL
More expressive
Rust pros
Very good package management and reproducible builds
Easy to compile single executable
Catches tons of problems already at compile time
Much more stable, not so many breaking changes
So I think writing the actual code will be easier in julia, while creating something that is callable from python and java will cause trouble.
I also have limited experience with Rust, but if I were you, the following excerpt from Rust compared to saying
P = [a b c d]
Q = [a b; c d]
in Julia gives me the answer at a glance:
let p = { // P = [a b c d]
let mut p = Array2::<f64>::zeros((n, 4 * n));
let n = n as isize;
p.slice_mut(s![.., 0..n]).assign(&a);
p.slice_mut(s![.., n..2*n]).assign(&b);
p.slice_mut(s![.., 2*n..3*n]).assign(&c);
p.slice_mut(s![.., 3*n..4*n]).assign(&d);
p
};
let q = { // Q = [a b ; c d]
let mut q = Array2::<f64>::zeros((2 * n, 2 * n));
let n = n as isize;
q.slice_mut(s![0..n, 0..n]).assign(&a);
q.slice_mut(s![0..n, n..2*n]).assign(&b);
q.slice_mut(s![n..2*n, 0..n]).assign(&c);
q.slice_mut(s![n..2*n, n..2*n]).assign(&d);
q
};
I think @jw3126 has a good list. I would add that the new package Julia package manager is probably much closer to having a “complete set” of package management features akin to what you have in Rust, but, on the downside, it’s brand spanking new and the documentation isn’t even complete yet.
As a (perhaps overly) general comment I would add that the people who are the most impressed with Julia tend to be people with primarily scientific and mathematical backgrounds who have spent a significant portion of their working lives doing computational problems. If you are really excited by what you see in DifferentialEquations.jl, JuMP.jl, Optim.jl, Flux.jl, RigidBodyDynamics.jl, or QuantumOptics.jl to name a few, you will probably really like Julia. If none of that stuff is intriguing to you (whether or not you would use any of them directly) you might not quickly find many super compelling reasons for using Julia over Rust. Even though the focus is on scientific computing, Julia is indeed a general purpose language, so in the long run hopefully it will be very appealing for other applications, but currently the ecosystem may or may not be there depending on what you are doing.
Julia has a robust (and relatively mature) C API, but the actual Python and Java wrappers of it may not be quite so robust. You can check out pyjulia for the Python wrapper, which should work, but those of us who work on Julia tend to put much more of our development effort into the Julia side.
As an aside, having used Julia routinely for more than 2 years, and having come from C++, Fortran and Python, I can tell you that the “real” reason to use Julia is multiple dispatch. This is really the thing that makes me never want to use other languages. Whether enough that is a compelling enough reason to use Julia over Rust will of course depend on what you’re doing.
My god that’s horrifying. It’s been a good while since I’ve looked into Rust now, but isn’t there at least some good array operations library for this sort of thing? My memory of what array operations were like is not quite so macabre (though it was certainly not nearly as nice as it is in Julia).
It is not that horrible, there are libraries for this. [a b] and [a;b] can become stack(Axis(0), &[a.view(), b.view()]) and stack(Axis(1), &[a.view(), b.view()]), see here.
Rust does “feel” like a lot of overhead in order to avoid the GC. Can you comment on / know how I can check whether the GC in Julia is going to cause an issue for me?
BenchmarkTools lets you run code, and it also record both allocations, and the percent of time attributable to gc activity.
julia> using LinearAlgebra, BenchmarkTools
julia> A = randn(2048, 2048);
julia> B = randn(2048, 2048);
julia> @benchmark $A * $B
BenchmarkTools.Trial:
memory estimate: 32.00 MiB
allocs estimate: 2
--------------
minimum time: 95.012 ms (1.26% GC)
median time: 96.191 ms (1.25% GC)
mean time: 97.023 ms (2.04% GC)
maximum time: 130.751 ms (27.51% GC)
--------------
samples: 52
evals/sample: 1
It’s also common practice in Julia to pre-allocate memory and operate in place, so that you don’t have to pay the price more than once.
julia> C = similar(A);
julia> @benchmark mul!($C, $A, $B) # like C = A * B, but overwrites the contents of C.
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 89.723 ms (0.00% GC)
median time: 90.397 ms (0.00% GC)
mean time: 90.657 ms (0.00% GC)
maximum time: 97.895 ms (0.00% GC)
--------------
samples: 56
evals/sample: 1
So with tools like BenchmarkTools and profiling, plus the convention of commonly providing in place / non-allocating versions of functions (stylistically shown by ending the name with a bang “!”) make it easy to avoid the garbage collector in hot regions.
If you’re dealing with lots of small calculations, you can also use things like StaticArrays, which are stack instead of heap allocated. I believe that is true for all user-defined structs without references.
I can give a really detailed answer, but the simple one is where to start. Do you want to do things interactively? Julia is much better there. Is getting a single executable more valuable to you? Then Rust/C++/D may be a better choice for the time being since as of now Julia doesn’t statically compile, and it may take a bit. Those languages are more built around the “building binaries” workflow anyways. Rust/C++/D lets you do a few things closer to the metal, so you can ask yourself whether you’d make good use of that.
Generally, I think of Julia as just a much better choice for me in the scientific computing area since everything pretty much as the same “speed limit” that’s not slow (so Rust, C++, D, Julia, Fortran etc. will give similar speeds if the code is optimized), but it has interactivity and generic coding which is sooooo much stronger than anything else. But if you want to go deep into custom allocators or something (that’s usually lower than what people do with scientific computing codes), R/C++/D allows some nice tricks. Usually I would think of them for systems programming or embedded instead because of where their pros lie.
Rust checks lifetime bounds propagation at compile time, so if in your code you reference a variable present in another context it simply does not compile. It’s not that you avoid the GC in Rust, but instead you have to manually specify for new datatypes and methods their lifetime bounds.
This slows compilation, of course.
To answer to the OP : it depends on what you need to do. About the two questions :
Linear Algebra : here I think that Julia wins easily. The only library for LinAlg that I have used in Rust is nalgebra, which will simplify matrix instatiation and operations, but nothing more.
The pros are that you can write applications for WebAsembly or Embedded platforms.
Julia instead natively incudes LAPACK and OpenBLAS, plus other libraries that exploit advanced linear algebra techniques.
Parallelism : Parallelism in Rust is great, as the borrow checker is capable of automaticcaly detecting possible races at compile time. Also Rust includes Channels for inter-thread communication and ownershinp transference, Shares States Mutexes, plus the possibility of redefining the Send and Sync traits for a custom type.
Julia has experimental threading and a very strong library for coroutines. For example if you need Concurrent I\O is better to use Rust, no I think that you must go with Rust as I\O in Julia is not thread-safe. Julia has a world-class support instead for shared data structures and computations splitted among multiple machines.
If in the realm of performance what scares you most of Julia is the Garbage Collector, stay with Julia. Unless that again, you needed to be independent from the OS syscalls, i think that especially for computational instensive applications it does not make sense to lose the benefits of OpenBLAS or many things like SIMD that Julia implements and are still experimental in Rust.
As a general rule : Rust is a system language (eg you can write a kernel in Rust and a OS) so comparing it to Julia is like comparing C and Perl, I mean they have different purpouses. As writing in Rust is not trivial, it will be difficult for you to find external libraries that fit your computational dreams.
On the GC, although I cannot comment on the situation over at Rust, I must say that I find it relatively simple to just completely avoid it by using some caches when it really matters. I find it also relatively easy to understand when there are going to be allocations and what to do about them (different datastructures such as StaticArrays etc). And I am not a CS wizard by any stretch of the imagination, so I think it’s also not that difficult to learn how to deal with.
Reading through these discussions, I get the impression that while Rust could be extended with all the relevant linear algebra operations and friendly syntax (as it is a powerful general-purpose language), this would involve quite a bit of work, which is not currently a priority at the moment for enough developers to make this happen quickly.
This is natural: language communities usually focus on capitalizing on the comparative advantages of the language first, to solve the problems which lead them to creating the language. Filling in all the niches for libraries usually comes later. This reminds me of a recent discussion about databases in Julia.
I echo @ChrisRackauckas’s point about interactivity, which would be very relevant if the algorithms of the library are not fully specified. While one can translate a known and well-tested algorithm to any language and in the worst case just use some BLAS/LAPACK bindings, exploratory programming is painful without interactivity.
My first programming language was JavaScript, then I learned Node.js (just more JavaScript), and then I picked up Julia after having experimented with Python for a short time. I’ve been learning Rust over the past couple of weeks and I must say that having a grasp of a high-level language like JavaScript and then getting a taste of a really low-level language like Rust really allows you to see the beauty of Julia. I think I’ll continue down the Rust road and probably use it as a hobby language (mostly for compiling to WASM) and I do think that some of the things that Rust forces you to learn will be very helpful in writing highly-optimized Julia code - but it’s so nice that Julia doesn’t force you to learn these things in order to get amazing performance.
Regarding to syntax, surely I love julia over all languages I know( include rust).
Regarding to libraries, you can search base on your personal need. Surely julia not applicable in all system/real-time application due to gc stop-the-world.
Regarding to computing model, you can refer(I also put c here as base line):
| | c | julia | rust |
| computation | v,o,c | v,o,c | v,o,c |
| | | | |
| abstraction | type | type | type |
| | struct | struct | struct |
| | function | function | function(support closure) |
| | macro | macro | macro |
| | | multi type(via union) | multi type(via interface) |
| | | module | module |
| | | generic | generic |
| | | method | method |
| | | | |
| restriction* | scope | scope | scope(support ownership*) |
| | mutability | mutability | mutability |
| | | | interface(trait) |
| | | | |
* restriction can limit expression, but on the other hand it means pattern, so template/automatic code can be generated by compiler.
the goal is to maximize gain from restriction.
* ownership can be seems as fine-grained scope control, which make function application as scope boundary. Rust use this restriction to generate memory management code in compiler