Comparison of Rust to Julia for scientific computing?

It is nice to note that a lot of the libraries for allocation/de-allocation use some kind of memory management for small objects, for performance reasons. They will allocate a “big” memory arena on initialization that will be assigned to small objects. When you free those objects, the memory is not really released, but marked as free internally on the library. If you ask for a big enough memory area, then the request for memory is passed to the OS.

This is a really level-down way to say how the allocation schemes work, but it’s enough to understand that even static memory languages tends to use some kind of memory management for performance reasons.

14 Likes

I am looking forward to transfer my current Python&C++ time to one single language time, Julia or Rust. I see a lot of scientific packages in Julia, while Rust got much less. Most of my work is scientific, plus a little embedded.

I think interactive is not the major reason for scientific programming, actually the compile time allows me to rest and think.

I do need my code to work consistently, the most hated bug I think is those which don’t happen every time, but only a few times when running a million times. I don’t even know how to debug them. That is why I prefer safety feature.

But I didn’t see the ecosystem of Rust building up, instead, Julia already got what I need on scientific computing.

Come on, Rust, if my transfer is from a combination of Python&C++ to Julia&Rust, that will be a diaster. I hate using two languages. I prefer not to transfer if that is the way it goes.

1 Like

Recently I started thinking about this possibility, and I am glad to see people who are more knowledgeable than I am are thinking the same way. If Python, which is less strict about types than Julia, can do it, why can’t Julia do it? It worries me quite a bit that my code can contain errors that will only be discovered while running some edge case. Of course testing would help dealing with this problem but i) it would have to be exhaustive and therefore time-consuming (for the coder), and ii) still there would be no guarantees.

1 Like

Do what? Julia can do all Python can. I’m confused, Python can fail at runtime (most languages have runtime errors, even Rust; Elm is a rare exception, and I’m not sure it’s better that e.g. divide by zero doesn’t fail there).

Do static type checking:

" Python will always remain a dynamically typed language. However, PEP 484 introduced type hints, which make it possible to also do static type checking of Python code.

1 Like

But you can do the same in Julia as well. To type annotate everything.

1 Like

I type annotate everything. But Julia will only catch the error when I run the program. The same as Python. But AFAIK the linter will not catch type errors. For example, it does not catch the following:

function foo(x::Int64)::Int64
    return x
end

function bar(y::Float64)::Float64
    return y
end

bar(foo(88))

or even

bar(88)

I hope I am wrong.

JET.jl is for catching errors without running the program. In this case, you’d simply do

#+begin_src julia
function foo(x::Int64)::Int64
    return x
end

function bar(y::Float64)::Float64
    return y
end

using JET
report_call(Tuple{Int}) do x
    foo(bar(x))
end
#+end_src
#+RESULTS:
#+begin_results
═════ 1 possible error found ═════
┌ @ /var/folders/8j/wwv2d08x00v_5ss1g4dq6f2r0000gn/T/babel-TsiDZU/julia-src-Qz7bHQ.jl:12 bar(x)
│ no matching method found `bar(::Int64)`: bar(x::Int64)::Union{}
└────────────────────────────────────────────────────────────────────────────────────────
#+end_results
9 Likes

Thaks. I wasn’t aware of this. Will give it a try.

Is there a Pkg that can check if I write a function that is not type-stable or if I change the type of a variable in the code?

You can use the built-in code_warntype, or for something more interactive Cthulhu.jl

1 Like

What version of Julia and JET.jl are you using? On version 1.8.0, running it on that snippet simply gives me

julia> using JET

julia> report_and_watch_file("./ex.jl"; annotate_types=true)
[toplevel-info] virtualized the context of Main (took 0.018 sec)
[toplevel-info] entered into ./ex.jl
[toplevel-info]  exited from ./ex.jl (took 3.416 sec)
No errors detected

Julia version 1.8.1 and Jet.jl v0.6.6.

julia> versioninfo()
Julia Version 1.8.1
Commit afb6c60d69 (2022-09-06 15:09 UTC)
Platform Info:
  OS: Windows (x86_64-w64-mingw32)
  CPU: 20 × 12th Gen Intel(R) Core(TM) i7-12700K
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-13.0.1 (ORCJIT, goldmont)
  Threads: 15 on 20 virtual cores
Environment:
  JULIA_EDITOR = code
  JULIA_NUM_THREADS = 15

[c3a54625] JET v0.6.6
1 Like

This subthread should probably be split to a new thread, but strictly speaking, this is inaccurate. Type hints are not intended to match or check Python’s much simpler types. This allows type hints to have cool stuff like parameters, which does help the static type checker find programming errors and let readers understand the intent of the code better.

This isn’t accurate either, type annotations in Julia actually do conversions, assertions, or dispatch depending on where they are, and they do match Julia’s type system.
They are not needed everywhere for static checking either, JET.jl uses Julia’s type inference to find issues so methods don’t need to be annotated so heavily like that foo/bar example. JET.jl’s documentation examples can probably give a better idea.

6 Likes

I forget about implicit conversions. Thanks for clarification / reminder.

I’m not a true expert but I’ll take a stab. I’m not saying that Rust is as good as Julia for scientific computing. But it is both fast and safe. Here’s an illustrative example:

fn main() {
    let a = [1, 2, 4];

    // Sum of the squares of all of the elements of the array
    let mut sum_sq = 0;
    for x in a.iter() {
        sum_sq += x * x;
    }

    // Another way:
    let sum_sq2 = a.iter().fold(0, |acc, x| acc + x * x);

    println!("Sum of {:#?} is {}", a, sum_sq);
    assert_eq!(sum_sq, sum_sq2);

    let message = "Hèllo";
    let e = message.chars().nth(1).unwrap(); // message[1] is not allowed.
    println!("Second letter of {} is {}", message, e);
}

Run this here.

I think both of these sum-of-squares functions are equally efficient, and just as fast as C. Possibly faster, since in some cases the compiler will automatically use SIMD instructions. Bounds checking does not slow this down. In each trip of the loop over a, it’s testing whether or not the iterator has reached the end, and that’s as fast as testing an index counter. It’s much more common to use iterators than explicit indexing.

Rust often gives you “zero-cost abstraction.” One manifestation is that these iterators and their many methods will compile down to code that you can’t hand-optimize any better. You get macros like println! which the compiler first expands into code. The print statement here is faster than printf since the format string parsing is done at compile-time.

Safety goes way beyond bounds checking. Variables and references are immutable unless you declare them with mut. The language allows and even encourages some functional programming paradigms, so you can write pure functions when it makes sense to. References (borrows) are checked by the compiler in a way that eliminates mistakes leading to segfaults and (most) race conditions in multi-threaded code. Built-in types like Option and Result allow for ergonomic error checking and completely eliminate the need for null values.

Another thing about safety and indexing with regard to strings: Why can’t you take message[1]? Because strings are UTF8 and each code-point is one or more bytes. So you can only really iterate. chars() returns an iterator, and nth() returns an Option. unwrap() on that will panic if the Option enum had the None value, which is a lot like bounds checking. Since I know I’ll get Some('è') and not None in this case, I just unwrap() instead of properly handling both cases. There are other libraries that let you iterate over grapheme clusters instead of chars.

I’m giving that example because it helped me understand some of the unusual ways that Rust strives for safety throughout its design. You can index a string directly in Julia, but it looks to me like internally Julia converts UTF8 to a 32-bits-per-char format. Rust can’t do that because as a systems language it has to remain as performant as possible. Sure, [n] is O(1) while chars().nth(n) is O(n), but you aren’t forced to copy incoming UTF8 buffers into an extra-wide array.

8 Likes

I don’t think so.

julia> sizeof("a")
1

julia> sizeof("ab")
2

julia> sizeof("θ")
2

You can index a string in Julia, but it’s not safe:

julia> "θρ"[2]
ERROR: StringIndexError: invalid index [2], valid nearby indices [1]=>'θ', [3]=>'ρ'
2 Likes

In Julia strings are stored in UTF-8, but when you get a character out it takes 32 bits.

5 Likes

I’m a bit late to this thread, but if anyone is interested in this topic, I will be hosting the Julia + Rust Birds of a Feather at JuliaCon this year, and Rust’s play into the HPC world will definitely be a topic there.

7 Likes