Live data from Hacker News

Rust Performance Pitfalls

llogiq.github.io

21–30 of 112 posts

Re: Rust Performance Pitfalls

#21
post #20
post #19

Earlier quoted context omitted.

The nature of warnings means that any time you make assumptions about those that might need the warnings having enough knowledge to correctly assess an ambiguity, you've likely just failed a portion of the people the warning was meant to help. The correct response when warning people of potential problems is never "oh, they should be able to figure out whether this applies to their case".

Right. The correct answer here is to convert external data into UTF-8 once and keep it that way. You're probably not going to become compute-bound, even if you're just reading in text and writing it out again. The UTF-8 check is linear time.

Most of the Rust programs I write are compute bound, and finding ways to avoid the separate UTF-8 validation step are critical to their performance.

Re: Rust Performance Pitfalls

#22
post #5

Earlier quoted context omitted.

There might be side effects from the first call to `collect`, so the compiler can't get rid of it without potentially changing the semantics of the program.

Best example of this (at least in Java): List futures = requestList.map(Client::makeRemoteCall).collect(toList); List responses = futures.map(CompletableFuture::get).collect(toList); Basically, we need to start all of the futures, then wait for all the futures. Eliminating the first call to `.collect` would force these remote calls to happen one at a time.

This is the kind of example I was hoping for, and I understand a little better now where collect() can have semantically-visible side effects. It seems like it should be possible to have a smarter type system that can tell the difference between futures where collect() can be optimized out and ones where they can't though.

Re: Rust Performance Pitfalls

#23
post #3

I'm not a compiler expert, but it seems like some of these should be unnecessary, especially with Rust's strong knowledge of types and ownership lifespans. Like this example from the article: let nopes : Vec = bleeps.iter().map(boop).collect(); let frungies : Vec = nopes.iter().filter(|x| x > MIN_THRESHOLD).collect(); where he recommends avoiding the first collect(). Can't the optimizer do that for you if you don't d…

On a high level it seems like it could, but collect() performs heap allocation (and size checks and reallocations for unknown-length iterators), and that's probably too big side effect for LLVM to ignore.

LLVM will remove heap allocations if they're unused. Optimizing the heap isn't something that LLVM refuses to do as a matter of principle.

Re: Rust Performance Pitfalls

#24

Earlier quoted context omitted.

'break your code in surprising ways' is super vague. Saying 'may cause out of bounds memory access/writes should the input not be valid UTF-8' explicitly is probably more scary.

The nature of undefined behavior is in fact super vague; it's not actually possible to say what will happen.

While technically true, a specific example helps the reader conceptualize what level of danger that means, when they don't already have a concrete understanding like you do. Recall that the breadth of experience for rust's userbase is much larger than e.g. C.

Re: Rust Performance Pitfalls

#25
post #9

Earlier quoted context omitted.

Not in the referentially-transparent sense. Most Rust functions meet almost all of the practical criteria for for purity (i.e. does not mutate global state (or otherwise any state that was not explicitly passed in to the function), does not do I/O), but that's only a comfort for the programmer's ability to reason about the code; this weakened notion of purity-by-default isn't enough to allow the typical optimizations…

but that's only a comfort for the programmer's ability to reason about the code A way to mark functions as pure for this purpose would be great! Especially if it's not as fraught as const in C++.

It wouldn't help in most of these cases. It'd be too burdensome to require that map/filter/etc. take pure arguments. In the absence of that, the compiler is left with examining the specific function being called, which it can already do as a direct call as long as the body is visible (#[inline]). If the body is visible, LLVM will internally automatically mark the function as pure (readnone) if it is.

Re: Rust Performance Pitfalls

#26

Earlier quoted context omitted.

Best example of this (at least in Java): List futures = requestList.map(Client::makeRemoteCall).collect(toList); List responses = futures.map(CompletableFuture::get).collect(toList); Basically, we need to start all of the futures, then wait for all the futures. Eliminating the first call to `.collect` would force these remote calls to happen one at a time.

This is the kind of example I was hoping for, and I understand a little better now where collect() can have semantically-visible side effects. It seems like it should be possible to have a smarter type system that can tell the difference between futures where collect() can be optimized out and ones where they can't though.

Something something Applicative vs Monadic code mumble handwave.

Actually that's part of why ApplicativeDo was created: https://research.fb.com/publications/desugaring-haskells-do-... It's pretty cool.

Re: Rust Performance Pitfalls

#27
post #20

Earlier quoted context omitted.

Right. The correct answer here is to convert external data into UTF-8 once and keep it that way. You're probably not going to become compute-bound, even if you're just reading in text and writing it out again. The UTF-8 check is linear time.

Most of the Rust programs I write are compute bound, and finding ways to avoid the separate UTF-8 validation step are critical to their performance.

That's because you write string search programs. Do those even need to run in UTF-8 space, as opposed to pure byte strings?

Re: Rust Performance Pitfalls

#28

Earlier quoted context omitted.

The nature of undefined behavior is in fact super vague; it's not actually possible to say what will happen.

While technically true, a specific example helps the reader conceptualize what level of danger that means, when they don't already have a concrete understanding like you do. Recall that the breadth of experience for rust's userbase is much larger than e.g. C.

Oh yeah, I mean, I'm not saying I disagree with adding detail, I can just see the impulse to not, since anything you say may or may not be true.

Re: Rust Performance Pitfalls

#29

Earlier quoted context omitted.

but that's only a comfort for the programmer's ability to reason about the code A way to mark functions as pure for this purpose would be great! Especially if it's not as fraught as const in C++.

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.

Re: Rust Performance Pitfalls

#30

Earlier quoted context omitted.

Best example of this (at least in Java): List futures = requestList.map(Client::makeRemoteCall).collect(toList); List responses = futures.map(CompletableFuture::get).collect(toList); Basically, we need to start all of the futures, then wait for all the futures. Eliminating the first call to `.collect` would force these remote calls to happen one at a time.

This is the kind of example I was hoping for, and I understand a little better now where collect() can have semantically-visible side effects. It seems like it should be possible to have a smarter type system that can tell the difference between futures where collect() can be optimized out and ones where they can't though.

> It seems like it should be possible to have a smarter type system that can tell the difference between futures

While that Java code is neat, you don't need to get anywhere near concurrency to exhibit problems with side effects. Here's a program with two iterator chains and two calls to collect:

    let foo = [1, 2, 3];
    
    let bar: Vec = foo.iter()
        .map(|x| {
            print!("{} ", x);
            x + 10
        })
        .collect();
        
    let qux: Vec = bar.iter()
        .map(|x| {
            print!("{} ", x);
            x * 10
        })
        .collect();
This program prints "1 2 3 11 12 13 ".

Here's that same program, but with the first collect removed and the iterator chains collapsed into one:

    let foo = [1, 2, 3];
    
    let bar: Vec = foo.iter()
        .map(|x| {
            print!("{} ", x);
            x + 10
        })
        .map(|x| {
            print!("{} ", x);
            x * 10
        })
        .collect();
This program prints "1 11 2 12 3 13 ".

When you have two iterator chains with two calls to collect, everything in the first iterator runs to completion, then the second iterator chain runs. When you collapse those two chains into one, you change the order in which things happen.

It's still true that a smarter type system would be able to track side effects, but like all effects systems, these things are viral, and it's not immediately clear whether the annotation burden makes up for itself in optimization potential.

Post reply on HN