Live data from Hacker News

The humble for loop in Rust

blog.startifact.com

51–60 of 60 posts

Re: The humble for loop in Rust

#51

Earlier quoted context omitted.

> `.map(...)` implies "I don't care about ordering, and therefore you don't need to, either" In plenty of languages, no such implication exist. `map` can be specified to run in a certain order.

Then that may not be an appropriate optimization for those languages.

That's what I'm saying. In fact, I think that's most languages.

Re: The humble for loop in Rust

#52
post #4

> Why is map so much faster? I am not sure. I suspect with the map() option the Rust compiler figures out it can avoid allocations altogether by simply writing over the original vector, while with the loop it can't. Or maybe it's using SIMD? I tried to look in the compiler explorer but I'm not competent enough yet to figure it out. Maybe someone else can explain! Yep, it's due to SIMD -- in the assembly for `using_ma…

Is there a reason why the loop and map don't result in the exact same code?

It looks like the code does exactly the same thing and something the optimizer could catch. Is is because of potential side effects? If not, maybe there is a ticket to open somewhere, if it isn't done already.

Re: The humble for loop in Rust

#53
post #37

Earlier quoted context omitted.

That surprises me, and I'd expect the FP version to be at least as fast, with the option to be much faster. With the for loop, you're saying "run against this value, then run against the next one, then run against the next one, then...". If the compiler isn't certain that the iterations aren't free of side effects, then it would have to run each one in order before moving on to the next. `.map(...)` implies "I don't…

Streams (FP in java) is slower than for loops for a couple reasons. A big one is java doesn't natively support real closures or lambdas. It does have syntax for them, but that is just syntactic sugar for an class with a single method under the hood. So streams end up doing lot of object allocation and garbage for the fake closures. Also, streams operate on objects, so they have to be on the heap. You can't use them w…

The other problem with the parallel streams is that it’s badly implemented. Threads for parallel streams are pulled from a single thread pool shared across the whole application so if you have multiple parallel streams in an application that’s already inherently multithreaded (e.g. a web service), you end up with severe resource contention that makes the parallelism work poorly if at all and can end up causing your app to deadlock because all the threads are in use somewhere else. There’s a workaround for it, but it ends up requiring some ugly boilerplate code to work.

Re: The humble for loop in Rust

#54

Earlier quoted context omitted.

That surprises me, and I'd expect the FP version to be at least as fast, with the option to be much faster. With the for loop, you're saying "run against this value, then run against the next one, then run against the next one, then...". If the compiler isn't certain that the iterations aren't free of side effects, then it would have to run each one in order before moving on to the next. `.map(...)` implies "I don't…

> `.map(...)` implies "I don't care about ordering, and therefore you don't need to, either" In plenty of languages, no such implication exist. `map` can be specified to run in a certain order.

The author is confusing .map(…) with the Map interface for collections.

Re: The humble for loop in Rust

#55
post #21

Earlier quoted context omitted.

It's not only because of SIMD. Contrasted to many other languages (though not all) the compiler is working with code here, not an arbitrary function pointer. In essence, JS and the like are operating with this: let result: Vec = list.into_iter().map:: >(Box::new(transform)).collect() Rust is able to inline the transform code right into the loop, which then becomes available for SIMD etc. Rust further brings really ni…

JS, Java and C# have vastly different implementation details each. Java uses type erasure for generics. .NET uses generic monomorphization for struct-typed generic arguments and method body sharing with virtual/interface dispatch for class-typed generic arguments (the types are never erased). Moreover, non-capturing lambdas do not allocate, and are also get speculatively inlined by the JIT behind a guard. It's a bit…

Java’s type erasure means that generic type information is not available at runtime.

C# lambdas: although non-capturing lambdas do not allocate, capturing lambdas do. "calls through them are virtual" is due to the underlying implementation of delegates in .NET.

Re: The humble for loop in Rust

#56
post #52
post #4

> Why is map so much faster? I am not sure. I suspect with the map() option the Rust compiler figures out it can avoid allocations altogether by simply writing over the original vector, while with the loop it can't. Or maybe it's using SIMD? I tried to look in the compiler explorer but I'm not competent enough yet to figure it out. Maybe someone else can explain! Yep, it's due to SIMD -- in the assembly for `using_ma…

Is there a reason why the loop and map don't result in the exact same code? It looks like the code does exactly the same thing and something the optimizer could catch. Is is because of potential side effects? If not, maybe there is a ticket to open somewhere, if it isn't done already.

The FromIterator impl for Vec is specialized with unsafe code for these cases.

FromIterator is the trait that the collect method uses.

Specialization isn’t a stable feature in Rust, but is used extensively in the standard library.

Re: The humble for loop in Rust

#57

Earlier quoted context omitted.

That surprises me, and I'd expect the FP version to be at least as fast, with the option to be much faster. With the for loop, you're saying "run against this value, then run against the next one, then run against the next one, then...". If the compiler isn't certain that the iterations aren't free of side effects, then it would have to run each one in order before moving on to the next. `.map(...)` implies "I don't…

> `.map(...)` implies "I don't care about ordering, and therefore you don't need to, either" In plenty of languages, no such implication exist. `map` can be specified to run in a certain order.

It's interesting -- so, logically, `map` in Rust does imply an ordering. The closure is a `FnMut`, i.e. a callback which mutably captures values, causing external side effects. And it is guaranteed that if externally visible, mutations will be done in order.

But `FnMut` is the most general possible thing you can pass in. In reality, most callbacks are pure functions that don't alter mutable state at all. With Rust's monomorphization and aggressive inlining, LLVM can figure out that there's no mutation going on and can optimize that.

There is a wrinkle here, which is that capturing variables mutably is one of two ways a function can have side effects in Rust. The other way is via interior mutability, through UnsafeCell [1], or, more commonly, a wrapper around it like Mutex or RefCell. In that case as well, Rust guarantees that function calls to map are done in order. Luckily, because UnsafeCell is the root of all interior mutability, the compiler can simply track whether an UnsafeCell is transitively involved.

If you're wondering where the humble `print!` comes in -- well, it clearly has side effects. But it acquires a global lock on standard output each time it's called [2], so UnsafeCell is involved.

[1] https://doc.rust-lang.org/std/cell/struct.UnsafeCell.html

[2] https://doc.rust-lang.org/std/macro.print.html

Re: The humble for loop in Rust

#58

Earlier quoted context omitted.

JS, Java and C# have vastly different implementation details each. Java uses type erasure for generics. .NET uses generic monomorphization for struct-typed generic arguments and method body sharing with virtual/interface dispatch for class-typed generic arguments (the types are never erased). Moreover, non-capturing lambdas do not allocate, and are also get speculatively inlined by the JIT behind a guard. It's a bit…

Java’s type erasure means that generic type information is not available at runtime. C# lambdas: although non-capturing lambdas do not allocate, capturing lambdas do. "calls through them are virtual" is due to the underlying implementation of delegates in .NET.

Well, yes, but delegates as a term is not often used in other languages so I did not mention them for simplicity's sake.

For what it's worth - the real issue in C# is not even the virtual calls but the way Roslyn caches lazily allocated non-capturing lambda instances. It does so in a compiler-unfriendly way due to questionable design decisions inside Roslyn.

Luckily, this has a high chance of changing in .NET 10. Ideally, by the time it releases hopefully the compiler will both understand the Roslyn's pattern of caching better and be able to stack-allocate non-escaping lambda closure instances.

Lambdas capturing 'this' inside instance methods of the object they refer to do not allocate either.

Re: The humble for loop in Rust

#59
post #13

Earlier quoted context omitted.

That's not accurate, it can be used while consuming an Iterator and, depending on the implementation, be used to guide the consumer during runtime. The stdlib likely is not doing this but the API very much allows advanced behavior. We, e. G., used this for some part of a query engine in a course in uni to guide algorithm choice for operators.

> The stdlib likely is not doing this Um, yes it is, extensively? SpecFromIterNested is a specialization trait for alloc::vec::Vec's FromIterator which handles both the TrustedLen and ordinary Iterator scenarios For an ordinary Iterator, it calls next() once to check this Iterator isn't done, if it's done, we can just give back a Vec::new() since that's exactly what was needed. Otherwise, it then consults the hint's…

> Um, yes it is, extensively?

Sorry, that was a mistake on my part. I did not see it explicitly in any of the code. Thank you for pointing out in detail where the stdlib uses this.

Re: The humble for loop in Rust

#60
post #4

> Why is map so much faster? I am not sure. I suspect with the map() option the Rust compiler figures out it can avoid allocations altogether by simply writing over the original vector, while with the loop it can't. Or maybe it's using SIMD? I tried to look in the compiler explorer but I'm not competent enough yet to figure it out. Maybe someone else can explain! Yep, it's due to SIMD -- in the assembly for `using_ma…

Not only SIMD but also size hints which avoid bound checks and enable preallocation or even allocation reuse. These are often missing when using for loops.
Post reply on HN