Live data from Hacker News

Wild performance tricks

davidlattimore.github.io

21–30 of 86 posts

Re: Wild performance tricks

#21

> Even if it were stable, it only works with slices of primitive types, so we’d have to lose our newtypes (SymbolId etc). That's weird. I'd expect it to work with _any_ type, primitive or not, newtype or not, with a sufficiently simple memory layout, the rough equivalent of what C++ calls a "standard-layout type" or (formerly) a "POD". I don't like magical stdlibs and I don't like user types being less powerful than…

> I'd expect it to work with _any_ type, primitive or not, newtype or not, with a sufficiently simple memory layout, the rough equivalent of what C++ calls a "standard-layout type" or (formerly) a "POD". This might be related in part to the fact that Rust chose to create specific AtomicU8/AtomicU16/etc. types instead of going for Atomic like in C++. The reasoning for forgoing the latter is [0]: > However the consensu…

> I think this optimization does what you say?

Cool. Thanks for checking! I guess the article should be tweaked a bit --- it states that the alignment has to match exactly.

Re: Wild performance tricks

#22

> Even if it were stable, it only works with slices of primitive types, so we’d have to lose our newtypes (SymbolId etc). That's weird. I'd expect it to work with _any_ type, primitive or not, newtype or not, with a sufficiently simple memory layout, the rough equivalent of what C++ calls a "standard-layout type" or (formerly) a "POD". I don't like magical stdlibs and I don't like user types being less powerful than…

> Great example of Rust being built such that you have to deal with error returns and think about C++-style exception safety.

Not really. Panics are supposed to be used in super exceptional situations, where the only course of action is to abort the whole unit of work you're doing and throw away all the resources. However you do have to be careful in critical code because things like integer overflow can also raise a panic.

Re: Wild performance tricks

#23
post #20

> It’d be reasonable to think that this will have a runtime cost, however it doesn’t. The reason is that the Rust standard library has a nice optimisation in it that when we consume a Vec and collect the result into a new Vec, in many circumstances, the heap allocation of the original Vec can be reused. This applies in this case. But what even with the heap allocation being reused, we’re still looping over all the el…

You're misusing the term "undefined behavior". You can certainly say that these kinds of performance optimizations aren't guaranteed.

[deleted]

Re: Wild performance tricks

#24

> It’d be reasonable to think that this will have a runtime cost, however it doesn’t. The reason is that the Rust standard library has a nice optimisation in it that when we consume a Vec and collect the result into a new Vec, in many circumstances, the heap allocation of the original Vec can be reused. This applies in this case. But what even with the heap allocation being reused, we’re still looping over all the el…

> Those optimisations that this code relies on are literally undefined behaviour.

You cannot get undefined behavior in Rust without an unsafe block.

> The compiler doesn't guarantee it's gonna apply those optimisations.

This is a different concept than UB.

However, for the "heap allocation can be re-used", Rust does talk about this: https://doc.rust-lang.org/stable/std/vec/struct.Vec.html#imp...

It cannot guarantee it for arbitrary iterators, but the map().collect() re-use is well known, and the machinery is there to do this, so while other implementations may not, rustc always will.

Basically, it is implementation-defined behavior. (If it were C/C++ it would be 'unspecified behavior' because rustc does not document exactly when it does this, but this is a very fine nitpick and not language Rust currently uses, though I'd argue it should.)

> So your code might suddenly become super slow and you'll have to go digging in to see why.

That's why wild has performance tests, to ensure that if a change breaks rustc's ability to optimize, it'll be noticed, and therefore fixed.

Re: Wild performance tricks

#25
I’d strongly caution against many of those “performance tricks.” Spawning an asynchronous task on a separate thread, often with a heap-allocated handle, solely to deallocate a local object is a dubious pattern — especially given how typical allocators behave under the hood.

I frequently encounter use-cases akin to the “Sharded Vec Writer” idea, and I agree it can be valuable. But if performance is a genuine requirement, the implementation needs to be very different. I once attempted to build a general-purpose trait for performing parallel in-place updates of a Vec, and found it extremely difficult to express cleanly in Rust without degenerating into unsafe or brittle abstractions.

Re: Wild performance tricks

#26

> It’d be reasonable to think that this will have a runtime cost, however it doesn’t. The reason is that the Rust standard library has a nice optimisation in it that when we consume a Vec and collect the result into a new Vec, in many circumstances, the heap allocation of the original Vec can be reused. This applies in this case. But what even with the heap allocation being reused, we’re still looping over all the el…

> Those optimisations that this code relies on are literally undefined behaviour. You cannot get undefined behavior in Rust without an unsafe block. > The compiler doesn't guarantee it's gonna apply those optimisations. This is a different concept than UB. However, for the "heap allocation can be re-used", Rust does talk about this: https://doc.rust-lang.org/stable/std/vec/struct.Vec.html#imp... It cannot guarantee i…

> That's why wild has performance tests, to ensure that if a change breaks rustc's ability to optimize, it'll be noticed, and therefore fixed.

But benchmarks won't tell us which optimisation suddenly stopped working. This looks so similar to the argument against UB to me. Something breaks, but you don't know what, where, and why.

Re: Wild performance tricks

#27

Earlier quoted context omitted.

> Those optimisations that this code relies on are literally undefined behaviour. You cannot get undefined behavior in Rust without an unsafe block. > The compiler doesn't guarantee it's gonna apply those optimisations. This is a different concept than UB. However, for the "heap allocation can be re-used", Rust does talk about this: https://doc.rust-lang.org/stable/std/vec/struct.Vec.html#imp... It cannot guarantee i…

> That's why wild has performance tests, to ensure that if a change breaks rustc's ability to optimize, it'll be noticed, and therefore fixed. But benchmarks won't tell us which optimisation suddenly stopped working. This looks so similar to the argument against UB to me. Something breaks, but you don't know what, where, and why.

It is true that it won't tell you, for sure. It's just that UB means something very specific when discussing language semantics.

Re: Wild performance tricks

#28

Earlier quoted context omitted.

> That's why wild has performance tests, to ensure that if a change breaks rustc's ability to optimize, it'll be noticed, and therefore fixed. But benchmarks won't tell us which optimisation suddenly stopped working. This looks so similar to the argument against UB to me. Something breaks, but you don't know what, where, and why.

It is true that it won't tell you, for sure. It's just that UB means something very specific when discussing language semantics.

I see. These optimisations might not be UB as understood in compiler lingo, but it is a kind of "undefined behaviour", as in anything could happen. And honestly the problems it might cause don't look that different from those caused by UB (from compiler lingo). Not to mention, using unsafe for writing optimised code will generate same-ish code in both debug and release mode, so DX will be better too.

Re: Wild performance tricks

#29

Earlier quoted context omitted.

This is an issue that you would face in any language with strong typing. It only rears its head in Rust because Rust tries to give you both low-level control and strong types. For example, in something like Go (which has a weaker type system than Rust), you wouldn't think twice about, paying for the re-allocation in buffer-reuse example. Of course, in something like C or C++ you could do these things via simple point…

> in something like C or C++ you could do these things via simple pointer casts No you don't. You explicitly start a new object lifetime at the address, either of the same type or a different type. There are standard mechanisms for this. Developers that can't be bothered to do things correctly is why languages like Rust exist.

And that is safer... how?

Re: Wild performance tricks

#30

I don't like relying on (release-only) llvm optimizations for a number of reasons, but primarily a) they break between releases, more often than you'd think, b) they're part of the reason why debug builds of rust software are so much slower (at runtime) than release builds, c) they're much harder to verify (and very opaque). For non-performance-sensitive code, sure, go ahead and rely on the rust compiler to compile a…

The particular optimisation for non-copying Vec -> IntoIter -> Vec transform is actually hard coded in the standard library as a special case of collecting an Iterator into a Vec. It doesn't rely on the backend for this.

Though this optimisation is treated as an implementation detail [1].

[1]: https://doc.rust-lang.org/stable/std/vec/struct.Vec.html#imp...

Post reply on HN