Comparison of Rust to Julia for scientific computing?

I don’t see how that counts as doing bounds checking in general at compile time. It enables you to do some compile-time checks.

Imagine that you hash a message input by the user at runtime, mod the hash by 256, and use the result to index a length-128 array. It is fundamentally impossible for the compiler to know whether that operation will be out of bounds. You have to perform the check at runtime.

Isn’t it the same thing as StaticArrays.jl? Probably a lot of scientific computing can be done that way (since one often works with fix discretisation sizes)… but adaptivity would be out.

In a sound dependently typed language you then have to provide a proof in the language that the result of mod is a valid index.

No, you can still index StaticArrays out of bounds (even if the size is part of the type signature).

1 Like

Rust is safer in a sense that it has a compile-time ownership based type system (for memory management), which makes it safer than C++ and C, where there’s no such check (C) or safety is enforced through the specific usage pattern of constructor (destructor) and scope rules (C++). Generally the cost of this system in Rust is minimal or even zero because all these checks are erased during compilation. The (runtime) cost is not always zero since this system doesn’t handle cyclic structures or other complicated lifetime well, so you need to maintain some extra runtime data to pass compilation. In summary, Rust is generally fast, not slow, because they don’t need to pay the cost most GC languages have to pay.

For bound checking, @goerch is correct. Eliminating bound checking automatically is impossible because it’s an undecidable problem. But in dependent typed languages, eliminating all bound checking at compile time is totally possible. It doesn’t contradict previous claim because this is not a fully automatic approach. It’s done by requiring every array access being accompanied by a proof, which is simply a witness that the index is in bounds. Note that in a DT language a proof is also a value, not something outside the system. So to access values in the array, you need to construct the index and also the proof (both are values). The proof can be erased during compilation. This may sound a bit tricky, but does work.

Back to the original question, I think it’s fine to use Rust for a numerical analysis course. Unless extensive usage of external numerical packages is unavoidable, they can simply implement everything they need from scratch, just like development in C++/Fortan. But since in a foreseeable future Rust won’t have a place in scientific computing, why not just simply force the students writing C++/Fortan/C if they prefer static languages…

4 Likes

I don’t know Rust, but does interactive development really work well here? I’m a bit suspicious since Julia is designed from the ground up for interactive use.

I think that what @sswatson is saying is that providing the proof, if possible, generating the index value of the proper index type, amounts to a runtime check, even though the bounds check itself is guaranteed by the compiler.

Back to Julia, it would be poor practice indeed to use @inbounds when indexing an array with user-supplied indices. You would likely only do it when using something like eachindex or have provided your own mental proof that index values are in bounds. Now code like that in a dependently-typed language would perform all the bounds checking at compile time–no requirement that you’ve thought through all the cases.

Curiously, a few weeks ago I asked on Zulip the same question about the possible role of Rust in scientific computing. @ExpandingMan shared this blog post he wrote some time ago where he explains his opinions about the use of Rust (and Zig) in this field: tvu-compare: rust and zig.

I don’t have direct experience with Rust (although I’m very curious about it and I’d like to learn more about it at some point), but my feeling is Rust is a good choice if you’re ok with using C/C++/Fortran in the first place: if you like that kind of workflow, then Rust can be a refreshing new option, without sacrificing speed. But if you need interactivity (probably a very common need in research), then it isn’t much useful on its own. People are still going to write wrappers in other high-level and interactive languages. This is for example the case for polars: it’s a popular dataframes implementation written in Rust, but most people use it through the Python wrapper. So no, Rust doesn’t really address the two-language problem, if you care about it.

That said, I think Rust can be a good improvement on C/C++/Fortran if someone wants to start a new low-level project from scratch, also taking advantage of the good build system. Probably we can image a future where some low-level libraries are written in Rust and we use them from Julia instead of C/C++/Fortran libraries. However, I should also point out that at the moment you can’t build cdylibs (shared libraries with C interface) for systems using the Musl C library (although my understanding is that this issue is being addressed and should be solved in the future), also you can’t build at all any Rust package for 32-bit Windows to be compatible with Julia runtime (and this issue is not going to be fixed). So cross-platform interoperability is actually a problem for platforms already supported by Julia.

11 Likes

Just one sidenote on the interactive case: Rust’s incremental compilation is very fast. Subjectively, when I am working on juliaup and am in a run-edit-compile-run loop situation, I often subjectively felt as if that was actually faster than a situation when I have to restart a Julia session…

18 Likes

If you really are sure that bound checks are your bottleneck (in some cases the optimiser removes the checks; also always profile before optimizing to avoid surprises), there is get_unchecked. In general, I do not see any reason why rust should suffer in performance compared to Julia. Rust is really fast. For example there is rustfft which at some point even beat FFTW.

I have written a fair share of rust code during my PhD, mostly writing simulation code. Your mileage might vary, but I highly enjoyed it and was very happy with the result. However, there are by far not as many specialised scientific libraries, so you are mostly on your own.

For array heavy code it is not as comfortable as Julia syntax wise. For example ndarray ends up feeling more like the numpy ecosystem. There is nalgebra as well. Const generics are a nice addition for array heavy code , as it allows to statically check your dimensions during compile time.

What I really do miss in Julia is a static type checker. When reasoning about your code it actually helps (me) a lot to nail down the data structures and types first and then implement the actual code.

I do not buy the „scientific codimg is mostly explorative and uses throw away code and therefore it’s legitimate to not care about best practices and code quality; quick and dirty for the win“ at all. It’s true that a lot of unfinished ideas are cast into code during an experimental phase. However, in my hurtful experience going through other scientist’s code, those quick and dirty codes tend to stay around and end up (their results anyway) in papers. But adhering to scientific principles also means to make sure your results are reproducible and as easy to comprehend as possible. And this includes your scientific code. Therefore, you need to write robust, bug-free, and readable code. Having a good type system that allows you to declare and specify your assumptions and invariants helps with that a lot.

In the end, in both dynamic and static type systems you have to worry about the same things, otherwise you get nasty bugs (faulty results). The difference is, with a static type system you need to be explicit.

Summa summarum: Yes, you can write performant scientific code in rust. Ergonomics and the missing ecosystem for scientific libraries can be a hassle (depending on your domain); however IMHO you tend to write better code (in the sense of robustness, reproducability, readability) if you are willing to (and you should be).

16 Likes

The culture of the community often emphasizes and pushes people to writing performant code, but the language itself does not, so it is a constant fight against entropy, pushing the other way. It requires discipline to write fast Julia code, in a way that it doesn’t for writing fast C/C++/Fortran/Rust.
Of these languages, Rust probably requires the least discipline, making it the easiest to write quality code, because of all the help provided by the language.

I did experiment with the Rust e-graph library egg, and compared it to the Julia library Metatheory.jl.
Egg was faster to compile.
Egg also ran to convergence on my problem in less time than it took Metatheory per iteration, making it several hundred times faster.

Many people within the Julia community can and do write high performance packages, but at least within the Julia community, there are also many people writing slow packages, too.
But, to what extant is that a bad thing, if people can easily – and with minimal resistance – write working, correct, code?
I heard from someone that a very popular Julia plotting library (GR.jl, the default backend to Plots.jl) was obviously written by a Python programmer without any real Julia knowledge. That it was filled with Python idioms, and changing just a few things made it 2x faster.
I think the major win here is that someone who apparently didn’t have any real Julia knowledge could still make a major (positive) contribution to the Julia ecosystem, hopefully without spending too much of their time.

I’m a speed nut and perhaps a perfectionist to the point of valuing principal over pragmatism. (Also, being a perfectionist does not mean my own code is perfect – it means I hate everything I’ve ever written for not being perfect. Please keep that in mind if I sound judgmental of other’s work; I am not trying to dismiss its usefulness and no less harsh w/ respect to my own.)
So while I would like to see a language that enforces this or holds the programmers hands to get there, I also need to remind myself that there are real pros and cons to these tradeoffs.
Perhaps in the future we can move more towards the best of both worlds, with libraries like JET.jl helping to analyze code and point out problems.
As Taku put it, writing performant code requires restricting yourself to an optimizer-defined subset of the language; would be great if we could have automated testing for this, making it easier to accept and review PRs, and for newcomers to learn
Also, it’d be easier politically to enforce requirements imposed by a tool. I’m not being a nitpicky jerk, it’s this tool imposing those draconian requirements and code changes!

Disclaimer: I only played around with Rust for a couple weeks (a few months ago), so I am very far from an expert and also do not have any experience with it (or C/C++/Fortran) in a production environment, maintaining a large code base, dealing with dependencies and regressions, etc…

Once it is written, it’ll be very difficult to justify to anyone else why it should be rewritten, regardless of quality. Thus, I do think there is a lot of value to getting it as correct as possible the first time.

40 Likes

but it’s much easier to improve a Julia code piece while staying in Julia, like many examples you see here on this forum, people can improve a fairly complex program without fully understanding the problem because the performance critical part can often be optimized.

Order of magnitudes easier than, to re-write a Python or R library in C/C++ then wrap it again in Python/R so it doesn’t break for downstream users.

2 Likes

I think another interesting static language choice would be Odin Language.
It has built in support for vectors and matrices with support for Generics with Multi Morphism which in practice can be very similar to Julia’s style.

It doesn’t have the exposure of Zig / Rust but it is still a very nice and well designed programming language.

3 Likes

I wholeheartly agree that having an expressive and performant language where you can stay inside the language is dope. In this regard, Julia is miles ahead of python for data sciency stuff. Python only works for the community because of compiled libraries that lift you from the crippling performance of python.

For example, when using pandas, one starts to shy away from using .apply (mapping a function over a column), although really useful, because it kills performance crossing from C to Python. I cannot overstate my joy when finding out that Julia DataFrame.jl columns are basically just common Julia arrays. So you can throw the whole ecosystem at them, without an eye blink, opposed to carefully selecting your operations in order to stay inside the librarie’s domain (and computing context).

Having said that, I find it more demanding to write robust and correct code in Julia for an above small sized code base than in a language with an expressive type system. As an illustration, think about refactoring. It’s a real brittle thing to do in a dynamic language; you can only hope you have written enough tests catching your newly introduced mistakes. Core Python does not even care if you accidentally pass a string instead of a float to a function - if you are lucky, it results in some runtime error, but it can be a silent bug as well.

In a language like Rust the compiler guides you in large parts through a refactoring, e.g. you split up a function, fix the type errors, and more often than not it just works™.

“Fixing the two language problem”, in the sense that it reduces the context switching cost necessary in for example (Science)Python is superb! However, in my opinion, it does not make Julia a general purpose language fit for scaling up your business. In my eyes, for this it foremost misses tooling/assistants for writing robust and scalable software - like a static type checker.

I’m probably victim of the confirmation bias, but I observe a trend towards statically typed and functional programming in the industry, because there is a need to manage software. Look at TypeScript for example, introducing gradual typing to JavaScript or Go getting generics or Rust’s success (none of them are functional languages, mind you, but some of them borrow heavily from that world). In my experience, the scientific computing community often times is a few steps behind in this regard. I think, they don’t know what they are missing.

In the end, both the software industry and science has a need for correctness and reproducibility. It seems to me that the industry begins to make use of the available technology supporting those aims, whereas the science community seems to focus more on producing results quickly. Oftentimes reproducibility and correct implementation is an afterthought. Try to reproduce the results of papers that made heavy use of a computer. In most cases, code isn’t even a part of the publication (for various reasons), so you just have to believe I guess.

13 Likes

Actually, the type system in Julia is one of the language’s real strengths. Julia has type inference and so you don’t need to specify the types if you don’t want to, but the facility is there if you want it.

Take, for example, my new award-winning function:

function add(x,y)                                                                                                          
    x+y                                                                                                                 
end  

When my function is used, Julia will determine the types required, and use multiple-dispatch.

add(1,2)           # integer version, returns 3
add(1.0,2.0)    # floating point version, returns 3.0

I can also define the function with types:

function add(x::T,y::T) where T <: AbstractFloat                                                                                                          
     x+y
end                                                                                                                  

Try the experiment again:

add(1,2)           # integer version, returns an error, no matching add
add(1.0,2.0)    # floating point version, returns 3.0

I am glad to hear that you are getting on well with Rust. However, Rust’s mission isn’t the same as Julia’s, and so the community’s efforts will be inevitably skewed in different directions. You can write a web browser in C#, but I can’t think of one.

2 Likes

One of my mental images for programming resembles sticking together a series of pipes of different shapes, colors and size. A function can be thought of as a black-box where (as long as you don’t have side effects) only the input pipes need to be matched up with output pipes from somewhere else, and you will have data flowing through the whole construct. I find a large part of structuring code is just making sure that the data flows unimpeded, and that’s completely separate from the logic happening in the function itself.

Static languages help a lot with the matching-up-pipes problem, because they can tell you what parts you can stick together, and what others you can’t. I find this decreases the mental effort a lot. Julia doesn’t help in that regard because output types are usually not constrained. This is sometimes a feature but I would say most functions I encounter in practice don’t need complete type genericness (it’s actually harder to program those). However, it does have all the ingredients for a system that would help the programmer with a more strict approach. So I hope that in the future, one can maybe “opt in” to a strict typing mode in some places (maybe on a per module level), which would require you to type all input and output arguments (or make them perfectly inferrable) and otherwise the code will not even compile. JET kind of does parts of that already, so I’m hopeful we’ll see more improvements in the future.

5 Likes

I think I should recap what was discussed (as I understand it):

  1. The cost of bounds checking can be avoided in Rust by compile-time “proofs” of correctness, which are somehow done in the type system. There is also get_unchecked, which is unsafe. So I think the conclusion is that it is possible to achieve high performance in Rust.
  2. Rust does not have much in the way of interactivity. It wasn’t stated but it sounds like this would be hard to add due to the nature of static typing. However, its incremental compilation is fast so in many use cases this isn’t such a big deal.
  3. The array indexing syntax is not as nice as Julia.
  4. The package ecosystem is not as developed (no DifferentialEquations.jl, Flux.jl, etc.). Autodiff wasn’t discuss but I suspect it’s not as developed in Rust as in Julia.
  5. It’s possible to use Rust from Julia and vice-versa. I suspect this won’t work for passing in custom types, e.g. autodiff.
  6. The safety of Rust might hav benefits for a large scale scientific computing project, as it avoids bugs.
21 Likes

Indeed, Julia has an quite expressive type system (also I am missing some features, like something equivalent to traits as a first class feature for example). Hence, in my book it is a pitty it’s missing a static type checker. Maybe we’ll get there some day (looking at you JET.jl)

My point regarding Julia is not :Julia is not fit for scientific computing. IMHO it is and it is doing a great job! My point is, it could do better in some regards and was responding mainly to the “scientific code is throw away code” sentiment in the thread, which I obviously strongly oppose to.

I am also not saying: “Write all your code in Rust”. I was only sharing my experience that you can write scientific code in Rust and although you loose some feathers, you also gain something. It is not necessarily a bad choice. It’s a tradeoff and depends on your use case.

10 Likes

If one is after speed, excluding other considerations, then yes, Rust is absolutely fine.

There are Rust bindings for Enzyme, and since both Julia and Rust are based on LLVM, it should be possible to let them talk with some LLVM magic. Enzyme developers are experimenting with cross-language differentiation.

6 Likes

Maybe that is being misinterpreted here. There is a lot of throw-away code in scientific computing, yes, and that is prototyping or simply proofs of concepts or experiments. Once we get a working version of the code, two paths are possible: be happy with it if it solves our problem, proves what we wanted to prove; or perfection it and optimize it for whatever reason (needs to be distributed, needs to be much faster).

Julia is good for allowing one to do both things in the same language. There may be well cases, on both the prototyping or optimization phases, where other languages are better if we were experts in those languages and we knew from start what we are aiming at. But then we need the glues, we loose the composability, code reuse, etc.

But these languages comparison always boil down to comparing speed (which is arguably the only objective measure) and it ends up that obtaining fast code is possible in all of them. What is probably a general wrong point of view to look at the language features, since I am completely sure that for most scientific code (probably for most code) getting the ultimate performance is not really relevant. We generally get obsessed with that because it is an addicting game.

What is much harder to evaluate is if arriving at a correct, useful, software is easier in one language than the other. To really get a feeling about that one needs a reasonable experience in all the languages compared.

18 Likes

I don’t have much to say about Rust vs. Julia for scientific computing; many excellent points have already been made. I will instead let the following code snippets speak for themselves Rust vs. Julia:

Create random passwords from a set of user-defined characters:

fn main() {
    use rand::Rng;
    const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
                            abcdefghijklmnopqrstuvwxyz\
                            0123456789)(*&^%$#@!~";
    const PASSWORD_LEN: usize = 30;
    let mut rng = rand::thread_rng();

    let password: String = (0..PASSWORD_LEN)
        .map(|_| {
            let idx = rng.gen_range(0..CHARSET.len());
            CHARSET[idx] as char
        })
        .collect();

    println!("{:?}", password);
}
// "1*MdrHw3L5cv)ODa@j*xb2OH46*SpZ"

vs. Julia’s:

reduce(*,rand(['a':'z';'A':'Z';'0':'9'], 30))
# "6N5UwKa0cf4MWvUnUel0xH6aukJDDf"

Or just (as suggested by @DNF):

join(rand(['a':'z';'A':'Z';'0':'9'], 30))

Create random passwords from a set of alphanumeric characters

use rand::{thread_rng, Rng};
use rand::distributions::Alphanumeric;

fn main() {
    let rand_string: String = thread_rng()
        .sample_iter(&Alphanumeric)
        .take(30)
        .map(char::from)
        .collect();

    println!("{}", rand_string);
}
// vlV7wJf4O4xQn9o17nfh6ePDlCXXrU

vs. Julia’s:

randstring(30)
# "P32RwOTS1Y5flBT4CvlVMNT5GGfaVh"
12 Likes