Live data from Hacker News

Rust Performance Pitfalls

llogiq.github.io

91–100 of 112 posts

Re: Rust Performance Pitfalls

#91
post #89

Earlier quoted context omitted.

You need "-C opt-level=3 -C target-cpu=native -C target-feature=+ssse3" I actually wrote about it last week: http://tmccrmck.github.io//post/rust-optimization-partii/

There's a little typo in your C implementation of popcount: you reference `x` when you probably meant `n`

Thanks. I fixed it.

Re: Rust Performance Pitfalls

#92

> With some investment into optimizations, matching or exceeding C’s speed should be possible in most cases. What class of algorithms is faster in Rust than it is in C?

I don't think it can be generalized like that.

Theoretically, Rust's memory ownership/aliasing rules are stricter and more granular than C's restrict, so some pointer-heavy code could optimize better. Rust is very good at inlining by default. Rust makes it easy to use stack-allocated structures. But C can do the same, it's just a matter of effort.

Both languages are low level enough that you can use them as a portable assembly and endlessly tweak them to one-up the other. If some C code doesn't optimize well you can write a more contorted code that will.

Re: Rust Performance Pitfalls

#93
post #92

> With some investment into optimizations, matching or exceeding C’s speed should be possible in most cases. What class of algorithms is faster in Rust than it is in C?

I don't think it can be generalized like that. Theoretically, Rust's memory ownership/aliasing rules are stricter and more granular than C's restrict, so some pointer-heavy code could optimize better. Rust is very good at inlining by default. Rust makes it easy to use stack-allocated structures. But C can do the same, it's just a matter of effort. Both languages are low level enough that you can use them as a portabl…

Yeah the only thing that Rust might have over C, in terms of really optimized implementations, is low-level idioms that C declares to be UB, but Rust declares to be defined (generally to be what x64 hardware does).

Maybe something that leverages signed overflow, overlong shifts, or type punning.

But if you have enough control over your codebase to mandate the compiler/flags its built with, then you can generally tell the major C compilers to act like Rust in these cases.

That said, the expected win for Rust over C(++) in practice is that you can be more "reckless", because you have a stronger type system protecting you from messing things up. A production-quality C(++) codebase might rightly do more copies, use more reference counting, and use less concurrency just because the risk of doing otherwise isn't worth the potential performance wins.

Organizations have limited resources to commit to optimizing/verifying code. Rust is intended to get you more bang for your buck.

Re: Rust Performance Pitfalls

#94
post #59

I have been doing some exploration of how well Rust optimizes Iterators and have been quite impressed. Writing a iterator to provide the individual bits supplied by an iterator of bytes means you can count them with fn count_bits >(it : I) -> i32{ let mut a=0; for i in it { if i {a+=1}; } return a; } Counting bits in an array of bytes would need something like this let p:[u8;6] = [1,2,54,2,3,6]; let result = count_bi…

How much of this is the Rust compiler and how much is generic LLVM optimization passes?

The compiler itself currently does only some basic optimizations, it's all LLVM.

However, the compiler does have perfect aliasing info, and it does provide a subset of this info to LLVM (I don't think LLVM IR currently supports more fine grained aliasing info being provided from the compiler). This does help certain optimizations; though I'm unsure if this one is one of them.

Re: Rust Performance Pitfalls

#95
post #29

Earlier quoted context omitted.

We actually did have this once, but it wasn't really worth it, so it was removed. https://news.ycombinator.com/item?id=6940624 is the HN discussion, but it looks like the link might now be wrong? It was also a very, very long time ago, and so today's Rust might be different enough that those reasons don't apply any more.

The reason at that point vis that there weren't any practical benefits and that it was preferable to wait for a more general mechanism. The first claim is dubious, but I can empathize with second, as long as it doesn't become tacked on. Haskell can do cool optimizations that make it feel like magic sometimes.

Note that a lot of these optimizations aren't necessary in Rust -- e.g. list fusion is only valuable in Haskell because mapping over lists is exposed as a one-shot operation that produces a new list. So `map map map list` is conceptually building 2 whole lists of temporaries you don't care about (and you often don't actually care about the last one either, in cases where you just iterate over it and discard it).

Meanwhile mapping in Rust generally takes an iterator and produces another iterator that will apply the given closure to the current element as it's yielded. So the naive codegen for iter().map().map().map().collect() is exactly what list fusion is trying to produce -- no temporary lists.

TL;DR: making "map" having the monadic `T[U] -> T[V]` signature is really expensive. ¯\_(ツ)_/¯

Re: Rust Performance Pitfalls

#96
post #93
post #92

Earlier quoted context omitted.

I don't think it can be generalized like that. Theoretically, Rust's memory ownership/aliasing rules are stricter and more granular than C's restrict, so some pointer-heavy code could optimize better. Rust is very good at inlining by default. Rust makes it easy to use stack-allocated structures. But C can do the same, it's just a matter of effort. Both languages are low level enough that you can use them as a portabl…

Yeah the only thing that Rust might have over C, in terms of really optimized implementations, is low-level idioms that C declares to be UB, but Rust declares to be defined (generally to be what x64 hardware does). Maybe something that leverages signed overflow, overlong shifts, or type punning. But if you have enough control over your codebase to mandate the compiler/flags its built with, then you can generally tell…

> That said, the expected win for Rust over C(++) in practice is that you can be more "reckless", ...

I'm glad to see somebody articulate this observation. SaferCPlusPlus[1] is meant to, in part, bring this benefit to existing C++ code bases. The question is, would a borrow checker for C++ make sense?

[1] shameless plug: https://github.com/duneroadrunner/SaferCPlusPlus

Re: Rust Performance Pitfalls

#97

> for i in 0..(xs.len()) { let x = xs[i]; // do something with x } > should really be this: > for x in &xs { // do something with x } I am curious why the compiler can't rewrite the former to the latter?

At least in simple cases, it generates almost identical code for both: https://godbolt.org/g/twuLd1

(I may have gotten something wrong, this is my first Rust program! Yay!)

Re: Rust Performance Pitfalls

#98
post #63
post #36

Earlier quoted context omitted.

To me "your code breaks in surprising ways" means something more like "the user sees gibberish/wrong results" and not "your program segfaults". Segfaulting is not actually a surprising result to me - I have seen it over and over on out of bounds access.

A buffer overflow doesn't necessarily cause a segfault - that's the problem! A guaranteed segfault would be a completely valid and safe way to handle a buffer overflow. But segfaults only happen when your program tries to access memory it does not own. It is improbable that an overflowing buffer is straight at a page boundary, doubly so if the buffer is allocated on the stack. Instead, you get gibberish output, an in…

Yes, the overflow either segfaults your program, or doesn't. It's not undefined. Computers don't perform random operations. Just say "it will produce buffer overflow" - that is absolutely a defined operation: the CPU will execute a load from an address that hasn't been defined at that place in code and will either contain leftover data, or refer to an unmapped page, triggering a segfault. This isn't quantum mechanics where the underlying physical reality is inherently undefined until you measure it (caveat: as far as we know, at least), this is just insufficient knowledge of hidden variables. I understand that the compiler might emit instructions under various assumptions, which is what we mean by "undefined behaviour" when those assumptions don't hold, but once there's actual machine code running on the actual CPU, everything is absolutely perfectly defined.

All the solutions and problems in computing stem from the fact that we're programming completely deterministic Turing machines that do exactly what they're told.

Re: Rust Performance Pitfalls

#99
post #98
post #63

Earlier quoted context omitted.

A buffer overflow doesn't necessarily cause a segfault - that's the problem! A guaranteed segfault would be a completely valid and safe way to handle a buffer overflow. But segfaults only happen when your program tries to access memory it does not own. It is improbable that an overflowing buffer is straight at a page boundary, doubly so if the buffer is allocated on the stack. Instead, you get gibberish output, an in…

Yes, the overflow either segfaults your program, or doesn't. It's not undefined. Computers don't perform random operations. Just say "it will produce buffer overflow" - that is absolutely a defined operation: the CPU will execute a load from an address that hasn't been defined at that place in code and will either contain leftover data, or refer to an unmapped page, triggering a segfault. This isn't quantum mechanics…

That's a very narrow view of the problem. There are so many moving parts in a program that overwriting an arbitrary chunk of stack is effectively random. Do you object to the term "random number generator" too?

Just saying "or doesn't" is not a useful description. The vast majority of horrible broken behaviors fit inside of that bucket, where it doesn't segfault but then goes on to do the wrong thing at an unexpected place.

And because we're using an optimizing compiler, you can't give a simple description like "overflows a buffer". When the compiler assumes your code is correct, it might output instructions that behave in 'impossible' ways when fed invalid data. For example an if/else that takes neither branch, or both branches, or code that verifies a number has a certain value yet outputs a completely different value. You can only make assertions about what a particular compile will do, and that's obsolete information immediately.

Re: Rust Performance Pitfalls

#100
post #86

Earlier quoted context omitted.

That's really impressive, but I'm a little surprised LLVM didn't optimize that inner loop to a popcnt instruction.

You may have to specify a flag like "-C target-cpu=native" -- not all CPUs support popcnt, so LLVM can't generate it without knowing details about the target. I can't get the compiler on godbolt.org to generate it either way, though.

Does Rust support FMV (function multi-versioning)?
Post reply on HN