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
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
What can Rust do that Python can't?
The first thing that pops to my mind is:
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.
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.
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++.
Come to Rust for the speed, stay for the robustness.
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.
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.
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.
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.
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
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.
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
.
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.
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.
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?"
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.