What can Rust do that Python can't?

Actually it has got to do with it, because I am wondering which language (whether it is Python or Rust I was not considering C/C++) to use for this project (and any future projects).

Thanks mate I will take a look at those :slight_smile:

1 Like

What can Rust do that Python can't?

The first thing that pops to my mind is:

Multi-threading.


You cannot easily do multi-threaded CPU-intensive tasks on Python, which is the main limitation of the language.

  • Granted, this is not a real limitation of the language per se, rather of the most widespread implementation of its interpreter, CPython, and its (in)famous GIL (Global Interpreter Lock).

  • Of course, if the task is no longer CPU-intensive, but is IO-bound instead, then you can do multithreading in (C)Python just fine.

    • This is especially true if you use asyncio & friends.
  • When you do not need the different parallel "tasks" to share memory (or just in some specific ways), then you can use multi-processing to circumvent the multi-threading issue, thanks to Python having implemented very ergonomic patterns to abstract away the inter-process communication.

  • Finally, "you can always write a native module" is the go-to suggestion to palliate any Python-specific limitations such as multi-threading CPU-intensive tasks.

At that point, there is a real tangible benefit from using Rust: for starters, Rust is itself a "native module" / "native language" (with which you can write Python modules), so the natural question that follows is: why go and write the rest of the application in Python when you could stay in the Rust realm?

To which there are definitely some good answers:

  • Python's interactive shell is a huge asset. I'd say it's the reason the ML world relies on Python so much, for instance.

    • In Rust there are some attempts to offer something similar, such as Google's evxcr ecvxcrxvccx evcxr, but it's always extremely challenging to offer an interactive session to a compiled language, and Rust is no exception, so it won't be as good as ipython, for instance.
  • you may be more comfortable writing Python than writing Rust, so for small applications / code logic (e.g., scripts), Python definitely has good appeal, and I'd even recommend to go for that if it helps you save time.

    • Counter-argument: one of the things that make it easy to program in Python is its lack of static analysis / "compile-time" robustness guarantees (you can use some linters such as mypy to palliate this issue too, but it's an ad-hoc fix that wasn't implemented within the language at the beginning, so such tooling has some limitations it will be hard for it to ever circumvent).

      This means that you may get your script up and running faster than in Rust, only for it to start failing or showing bugs in some scenarios later on, which leads to annoying debugging sessions and, when writing big / complex applications, to having to write tons of unit-tests to compensate for the lack of powerful static analysis (compared to Rust's, I mean).

This last point is the actual strongest point of Rust: you will notice that although getting code to compile can be challenging at first (but I guarantee you that after a while coding in Rust becomes second nature; I personally write Rust code as fast as Python's, nowadays), once the program compiles, it has far less bugs than a prototyped Python equivalent, or, if we go back to the multi-threaded world, far less parallelism bugs than that of a native module written in C or C++.

TL,DR

Come to Rust for the speed, stay for the robustness.

11 Likes

Lots of languages can be "compiled to native code", but that doesn't make them similar in performance or capabilities. Often it's not even close, and there are 10x or even 100x differences in performance among compiled languages.

Heck, even Rust itself is "compiled to native code" in Debug and Release modes, and these two modes, within the same language, can have 100x performance difference. It matters a lot how the languages are compiled in the tiniest of details.

That's because languages with a lot of possible run-time dynamism get compiled to inefficient native code that still has to preserve the dynamism and implement abstractions allowed by the language. Even if it's a native code, it may need to perform runtime checks and use runtime polymorphism that makes many optimizations impossible. There are some techniques (type inference, hiddem classes, devirtualization) that may help in certain cases, but still a lot depends on guarantees given by the language.

There's also a matter of having a runtime. Many high-level languages have tools that create "native executables", but that just bundles the VM with program's sources together in one file. It still runs a dynamic language in a virtual machine, which makes it unsuitable for lower-level tasks like drivers, codecs, or kernel extensions.

8 Likes

Wow I am surprised it is a huge difference.

I don't know if 100x is typical. I see 5x on my application, and I'm delighted to get it. Maybe I should work harder on tuning it.

1 Like

It really depends. If you’re doing a lot with arrays that just end up using numpy or pandas under the hood the speed up may be pretty minimal. If it’s mostly working with python objects a 100x speed up would not be surprising.

1 Like

Take code like this for example:

pub fn iter() -> u64 {
    (0..10000).map(|x| x*2).sum()
}

It can compile to 600 lines of assembly that makes 10000 calls to Iterator::next, Iterator::map, calls to the closure, additions to get the sum, with overflow checks on every step. It's all compiled to native code, but still performs a lot of work.

Or it can compile to just return 99990000:

because in Rust there is no "monkey patching" and the compiler can know exactly how map and sum behave, and predict their result at compile time.

7 Likes

That got me wondering. So I tried release and debug builds of a convolution over 20 million elements in various Rust implementations, serial and parallel execution. Here are the results:

Implementation                Debug Time(ms)     Release Time(ms)  Speed up

Implementation                Debug Time(ms)     Release Time(ms)  Release build Speed up

zso::convolution:             380640             24195               16
zicog::convolution_slow:      410088              8379               49
dodomorandi::convolution:     475962              8086               59
zicog::convolution_safe:      368243              7980               46 
Abjorn3::convolution:         900673              6619              136
pthm::convolution:            462066              1234              374
zicog::convolution:           428833              1228              349
alice::convolution_serial:    450238              1238              363 
zicog::convolution_fast:      399970              1222              327
alice::convolution_parallel:  120601               375              321     

Really, over 300 times slower in debug mode.

This does highlight that the compiler can have a really hard time optimizing your code if you don't write it "just so".

Codes are here if you want to play: GitHub - ZiCog/rust_convolution

3 Likes

Wow that is quite insane how much of a difference it can be.

I reordered the list, from slowest to fastest in final release build performance.

Note how the faster codes get the most boost from debug to release build as the optimizers kick in. For the slower versions the optimizers can't do so much.

Does it take longer to build the released version?

Generally yes. Depends where you start from, a build from clean or a rebuild after some change:

$ cargo clean
$ time cargo build
   Compiling ...
   ... a ton of crates...
    Finished dev [unoptimized + debuginfo] target(s) in 13.66s

real    0m13.699s
user    0m41.656s
sys     0m15.641s
$ touch src/main.rs
$ time cargo build
   Compiling convolution v0.1.0 (/mnt/c/Users/zicog/rust_convolution)
    Finished dev [unoptimized + debuginfo] target(s) in 2.77s

real    0m2.806s
user    0m1.000s
sys     0m1.891s
$ time cargo build --release
   Compiling ...
   ... a ton of crates...
    Finished release [optimized] target(s) in 20.63s

real    0m20.677s
user    1m59.688s
sys     0m15.203s
$ touch src/main.rs
$ time cargo build --release
   Compiling convolution v0.1.0 (/mnt/c/Users/zicog/rust_convolution)
    Finished release [optimized] target(s) in 2.93s

real    0m2.974s
user    0m7.672s
sys     0m1.000s
$

I don't worry too much about build times. I spend most of my development time running "cargo check" which is pretty quick. When everything is eventually straight I do a proper build.

1 Like

I see.

But what if you have a logical error? wouldn't it be better to build it in debug mode to check for logical errors as cargo check only checks for compilation errors?

When I write Rust code, I'll generally have several rounds of fixing compiler errors or warnings before I get back to something that compiles. Once it compiles, I'll go ahead and do a cargo build of course.

There's nothing stopping you from calling cargo build just because you already did cargo check.

1 Like

This is Rust we are talking about.

It can take me all day from having any clue what I want to do to actually getting anything to compile cleanly at all!

Likewise when I refactor existing code a bit.

My work flow is: Add a few lines, cargo check and edit it for an hour before it comes clean, add a few more lines, cargo check and edit it for an hour before it comes clean.....

Eventually I have something for for even the most cursory test.

The cruelest thing is when type inference is not able to deduce types and your not sure what types you are dealing with. Then you have to get a page or two of code in place with everything aligned, just so, and then it compiles.

Of course logic errors are always very possible. Which I won't find out till I have been arguing with the compiler all day.

Curiously, logic errors seem to be reduced by all this effort. Rust insists you think about what you are doing, organize things nicely and pay attention to where your data is and where it is going. By the time you have reasoned about all that things that compile have a lot more chance of actually working correctly.

2 Likes

How come you deleted your comment? I liked your comment.

It was just a general rant I posted without reading the rest of the topic. But after reading the topic, I don't think it's all that relevant in this case. OP's question is about audio processing libraries, not the merits of rapid prototyping and development speed vs maintainability.

This is an excellent comparison of Rust and Python. Kudos!
One of my early Rust projects was a for-fun game optimization program. I wrote it in Python first because I'm more familiar with Python. But I needed it to run much faster, and I wanted to practice Rust. So I re-wrote most of the code in Rust. The Rust version actually ran 1,000 times faster! And that's before parallelization, which was relatively easy with Rust and gave me another factor of 4-32 (depending on the system I ran it on). This is an extreme example and not completely fair to Python because when I re-wrote the code I redesigned some critical parts of it with speed specifically in mind. But the changes weren't to the algorithms, only the data structures to reduce memory allocations. In Big-O notation they were identical.

1 Like

Less is More

While there are good answers that directly address your question, another question you should be asking is "What can Python do that Rust intentionally can't?"

  • Rust does not allow you to create cycles in memory (not without a lot of effort, at least). Python can create cycles.
    A cycle is when object refers to another object, that in-turn refers to the first object: A refers to B, B refers to A. This initially makes Rust very challenging, but it really does result in code that is easier to reason about in the long run.
  • Rust does not allow you to hold a mutable reference and a immutable reference at the same time. Python always uses mutable references, so it always allows this.
    Again, this takes getting used to, but really does result in code that is easier to reason about.
  • Rust does not allow staggered object lifetimes. Python does allow this.
    This is really important for scenarios where destroying/dropping an object results in something like deleting a temporary file (or closing a file handle).
2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.