[C++ code]: https://github.com/parallel-rust-cpp/shortcut-comparison/blo...
[Rust code]: https://github.com/parallel-rust-cpp/shortcut-comparison/blo...
[C++ code]: https://github.com/parallel-rust-cpp/shortcut-comparison/blo...
[Rust code]: https://github.com/parallel-rust-cpp/shortcut-comparison/blo...
let pack_simd_row = |(i, (vd_stripe, vt_stripe)): (usize, (&mut [f32x8], &mut [f32x8]))| {
for (jv, (vx, vy)) in vd_stripe.iter_mut().zip(vt_stripe.iter_mut()).enumerate() {
let mut vx_tmp = [std::f32::INFINITY; simd::f32x8_LENGTH];
let mut vy_tmp = [std::f32::INFINITY; simd::f32x8_LENGTH];
for (b, (x, y)) in vx_tmp.iter_mut().zip(vy_tmp.iter_mut()).enumerate() {
And I tend to write stuff like this in Rust too. The loops especially. It's just so easy to throw together all those iterator combinators and get some ugly blob that's going to make people's eyes glaze over.`pack_simd_row` is lambda: `|arguments: types| { body }` Arguments are: * `i` of type `usize` (unisgned size_t), * tuple (anonymous struct) with two fields: vd_stripe and vt_stripe, which are modified inside of lambda. They are references to fixed size array of 8 floats.
Inside function we have loop over result of iterator, which produces tuple with two fields: `jv` and tuple with two fields: `vx` and `vy`. `vx` and `vy` are elements from `vd_stripe` and `vt_stripe`. `jv` is their index.
Inside loop we create two temporary mutable variables: `vx_tmp` and `vy_tmp`, which are fixed size array of 8 floats, which are initialized with infinity.
Then we have next loop, which goes to modify these temporary arrays in place.
And so on.
Being able to fit part of a program clearly into 5 lines with fewer characters makes it easier to ensure correctness than an algorithm spread over 10 lines that is full of extra "noise". It's why there's so much syntactic sugar in so many languages.
By contrast the Rust example uses parallel iterators from the Rayon crate. If the C++ example had something similar using a C++ library, it would probably be worse.
It's not Rust vs. C++, it's OpenMP directives vs. explicitly writing multithreaded code.
The C++ code looks like straightforward "C code" because it's using the OpenMP parallel for. If it dealt with threads explicitly it would not look as pretty.
I do agree that the Rust code has some verbosity, for example there are a lot of type annotations and using named constants instead of magic values.
For example (C++):
int na = (n + 8 - 1) / 8;
Versus (you could write this like the above too): let vecs_per_col = (n + simd::f32x8_LENGTH - 1) / simd::f32x8_LENGTH;
An example of the type annotations (are they really necessary?): let pack_simd_row = |(i, (vd_stripe, vt_stripe)): (usize, (&mut [f32x8], &mut [f32x8]))| {
Comparing the actual internals of the algorithm is quite straightforward, e.g. look at the middle loop (lines 70-82 in Rust vs. 58-97 in C++). Do note that the C++ code is loop unrolled by hand which makes it look simpler.In the Rust code, there's a pretty cool feature where multithreading can be toggled on/off with a compile time flag, see parts with: #[cfg(not(feature = "no-multi-thread"))]
If the C++ example wouldn't use OpenMP, this would be pretty impossible to achieve without being noisy too.
Also note that even with OpenMP, it would be quite easy to create a memory unsafety issue in the C++ code by for example accessing an array out of bounds inside the loop body.
Apart from these, I have no problems reading and understanding the C++ or Rust code (and I'm a noob with Rust).
OpenMP is suitable for the most simplest kind of parallelism in the first place.
If you look at the example code here, there's like 5 lines of pretty simple Rayon code that's almost a drop-in replacement for single threaded iterator code. It performs almost as good as the OpenMP C++ code, and it's guaranteed to be safe.
OpenMP is not C++ specific, it is also supported in fortran and C (although the newer OpenMP standards have first class support for C++ iterator semantics). In principle it could also be supported in rust although I assume that making it work with the borrow checker might not be easy.
The Rust code is... typical Rust. The algorithm almost gets lost in the specific peculiarities.
An interesting question in this context would be, is there anything preventing adding an OpenMP extension to Rust?
Regarding choice of implementation, there are more C++ compilers that support OpenMP than rust compilers.
OpenMP is designed around C-style for loops, which Rust discourages.
In this snippet the author went through some insane optimization, and opted to use quite a few attributes, those definitely make it look noisy in this case. FFI bindings are verbose and not so clean either.
This is not true. We agonized over syntax decisions. "fn" was actually preferred by most of the Rust community.
The Swift team probably also agonized over syntax decisions and they came up with something very different which has won over many people and attracted little criticism.
Swift also doesn't have pervasive use of lifetimes like Rust does. Lifetimes, by their nature, always add some level of "noise".
At the end of the day though, these tiny thing like fn, or the lifetime annotations and the many others add up to an unpleasant experience and less readable code.
If it becomes a mainstream language (like Java, C++, etc), I'd be happy to concede I was wrong. It could be that memory safety will trump ergonomics and it wouldn't be the first time that somewhat painful to use tools become very popular - see Linux, git, etc.
"For new features, people insist on LOUD explicit syntax. For established features, people want terse notation."
fn is fine.
IMHO Rust may have tried to roll a little too much into the language. It has the air of the second system effect to it, where every good idea from every language is added together to get something that is less than the sum of its parts and you get code that is hard to decipher until you've learned a full college course worth of syntax.
Can you name a specific feature you want removed from Rust?
An example:
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
I'm sure experienced Rust devs look at that and say it's fine, but that's definitely a hurdle for new developers. fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
https://tomtongue.com/docs/rust-wk-20.html#trait-bounds int size();
Am I declaring a function returning an int? Or defining an object of type int and invoking the default constructor?But if you spend enough time with the language it gets easy enough for these to not be a concern. I personally think it's very much worth learning.
It’s almost like Rust starts from a functional programmer’s perspective and adds the necessary complexity to have a “low level” language, whereas C is a simplified assembler and C++ tries to add “high level” amenities.
Example:
struct HReq<'a> {
}
struct HReq<a: LifeTime> {
}
Isn't that better?It’s not a language level constraint to name lifetimes with a single letter. But folks tend to not use longer names for good reason.
Rust is in a weird spot because we have two different kinds of generic parameters: types and lifetimes. They need to be distinguished from one another somehow. Nobody loves the lifetime syntax, but nobody has ever proposed something that would end up significantly better.
I’m not proposing you should use ‘a_lifetime, I’m saying you could. In the end, it ends up obscuring more than helping.
That example isn't too bad once you're used to the syntax and idioms but the lack of white space to space out logical blocks is what's making it hard to read.
Well, that and only what I can say equates to writing C++ in rust, much the same way you can write php in JavaScript if you squint hard enough.
Probably doesn't help it's trying to be a comparison which doesn't really work. A lot of that could be refactored out into idiomatic rust and it wouldn't look so bizarre.