Comparing k-NN in Rust
huonw.github.io
huonw.github.io
Thanks!
Edit: to be more clear, I wouldn't expect it to have an impact on performance, but I'm not convinced this is a good example of expressiveness and safety in the same package.
(Notably, since it's all in the types, you're guaranteed to not ignore any errors.)
- `int` is the fastest available integer type in C(++), whereas in Rust it is defined to be pointer-sized. This allows the C++ version to vectorize better than Rust by using 32-bit integers (also there aren't native SIMD 64-bit multiplications in current x86).
- Even when changing the native type from `int` to `i32`, to match the C++ code, rustc does not vectorize the distance function. I do not know the exact reason for this, but I would guess the iterator code is unable to elide null pointer checks. It's unclear to me whether this is a language limitation or simply a current compiler limitation.
Perhaps the best thing to do is to change zip, or add a new parameter to zip, to allow it to "chunk" its inputs and thereby enable vectorization. Or we could have a special type that encapsulates two or more vectors and checks up front that they're the same length, so that zip can then assume that they're the same length. It also might be possible to add an optimization to LLVM to have it figure out that the vectors are the same length.
zip :: [a] -> [b] -> [(a, b)]
Not: zip :: [a] -> [b] -> [(Maybe a, Maybe b)]
If you want to allow processing a sequence of tails, you could have: zipWithTail :: [a] -> [b] -> ([(a, b)], Either [a] [b])
Which would return a list of pairs and a list of either the tail of the first or the second input, whichever was longer.There is an issue to remove the notion of a "default" integer type.
But note that the C++ version has no bounds checks, so it's unsafe. You could always write C in Rust in the unsafe sublanguage, so saying it's "fundamentally inferior" is not true in any case.
Code at http://ix.io/cUd
I've looked at the output of both inner loops, and it seems I put too much faith in std::valarray's expression templates. Changing the distance function to
int distance(std::valarray<int> const& x, std::valarray<int> const& y) {
return inner_product(begin(x), end(x), begin(y), 0,
std::plus<int>(), [](int x, int y){ return (x-y)*(x-y); });
}
results in the exact same inner loop as Rust. The remaining speed difference comes from the file loading code, which admittedly is crap (way too many memory allocations). So for all intents and purposes, I admit defeat.Possibly. There's been some talk in that direction, but also some "can't we just fix llvm?" objections. I guess someone needs to write up a pull request (and see what other iterator traits to implement, maybe).
http://re-factor.blogspot.com/2014/06/comparing-k-nn-in-fact...
EDIT: now runs in parallel and is faster than the Rust single-threaded version, and on par with the parallel Rust version, but only ~35 lines.
[2] http://www.reddit.com/r/haskell/comments/27tcvz/knearest_nei...
* Rust's strong type system means the pieces fit together only in specific ways - roughly, if you can get it to compile again it will work; in more dynamic languages you have little confidence that the code works until runtime.
* Co-evolving the language with the downstream community is such a critical issue that Rust is developing several tools and processes to help, and this should set it apart from other open source languages that have gone down this path:
The Rust process already attempts to tag all breaking changes in the commit log with `[breaking-change]` and we've heard anecdotally that this has made forward-porting Servo much easier. This log isn't published anywhere besides the commit log yet, but it will be.
Secondly, Rust has a [stability](http://doc.rust-lang.org/rust.html#stability) system that tracks API stability at a fine level. This is influenced by node.js, but in Rust stability is detected and use of unstable API's can be enforced by the tooling. This is still in development but you can see it in the [docs](http://doc.rust-lang.org/std/intrinsics/).
What I've noticed from watching the Python, Ruby and Javascript module ecosystems approach and in some cases surpass CPAN (in terms of raw numbers of packages) is that there's a lot more programmers out there now, and a lot more existing implementations available in similar languages for just about anything you are attempting to do.
Has anyone here who uses it run into any downsides in comparison to other languages (aside from it being new and changing)?
tl;dr: If you consider "has a fairly complex concept that is unfamiliar and necessary to learn with a fair degree of depth" a downside, then I think it has one.
When I first started with Rust (was also my first foray with lifetimes), the compiler completely kicked my ass for at least a few days. I struggled a lot with writing anything beyond a few functions, and especially when those functions were returning borrowed pointers. I think the code I wrote at the point could be fairly characterized as, "the bare minimum that I could convince the compiler to accept."
But as I wrote more code, I got better at it pretty quickly. At this point, I can look at most Rust code and feel pretty good about spotting lifetime errors before consulting the compiler. I'd say it only took me a couple thousand lines of code to get there, which isn't a huge price to pay.
Anyway, this is obviously a personal anecdote. But it's coming from someone who thought Rust was crazy complex only a few months ago. FWIW, it took me about 48 days from knowing absolutely zero Rust (other than random Internet buzz) to writing and getting libregex merged upstream.
http://blog.ezyang.com/2013/12/two-bugs-in-the-borrow-checke...
This is basically why I said if you consider this a downside. I think it's only a "downside" in the same way that purity is a "downside" in Haskell - it isn't accidental complexity, and wrapping your head around it is just a part of really learning the language.
let distance (a1 : int array) (a2 : int array) =
let open Array in
let len = length a1 in
let acc = ref 0 in
for i = 0 to len - 1 do
let v1 = unsafe_get a1 i in
let v2 = unsafe_get a2 i in
let d = v1 - v2 in
acc := !acc + d * d
done;
!acc
the OCaml goes 3 times faster. This is what would be produced if OCaml's inliner had triggered on the original definition of `distance`, so that is probably the main difference in the two language's performance. If you inline some of the other functions by hand (and tidy up some of the sillier parts of the OCaml code) it easily runs 4 times faster than the original.In my mind this style should be the default for error handling when coding (i.e. I'd prefer to push the choice of using .unwrap or not to the caller)
The only place in the code, that I can spot, where `move_iter` is even available is on line 46. But I don't see any compelling reason to use move_iter there (plus, `validation_sample.len()` in the final println would have to be moved up and let bound before the call to move_iter).