Live data from Hacker News

Zero-cost futures in Rust

aturon.github.io

151–160 of 348 posts

Re: Zero-cost futures in Rust

#151
In my opinion - as someone with some background in CS - the name "future" is a little too overloaded here. It is not only used for the deferred computation of a value so much as it also means the composition of computations. This is not wrong per se, but calling the result a "future" alone oversimplifies what's happening below and hides some properties about the combinations.

The first observation one can make - which is not mentioned anywhere in the article - is that the composition of futures here can be understood as a monadic composition. This by itself gives a big hint why this interface is so powerful. Second is that this library could be understood as an implementation of process and process combination from pi-calculus [1] - sequential combination, joining, selection, etc - so it could be formalized using its process algebra.

From the practical side, one example of a mature library that implements similar concepts is the LWT [2] library for OCaml, which has the same idea of deferred computation, joining and sequencing, but calls the computations "lightweight threads". One could also argue about naming in this case, but it seem to reflect a better the idea of independent "processes" that are combined on the same address space.

Finally, as much as these concepts of futures and processes look similar on the surface, they each have their own properties - so it's always good to consider what better fits the model. By looking at the research and at other similar solutions, one can make more informed choices and have a better idea of what to expect from the implementation.

[1] http://www.cs.cmu.edu/~wing/publications/Wing02a.pdf

[2] http://ocsigen.org/lwt/manual/

Re: Zero-cost futures in Rust

#152
post #84

As mentioned in the post, given Rust wants to operate in the same space as C, this approach makes sense. However from a higher level, building more complex concurrent systems, dealing with futures/deferred-s/promises and/or a central select/epoll/kqueue reactor loop gets daunting and doesn't mix with complex business rules. Deferred based approach has been a round for many years. I experienced it by using Twisted (Py…

> So wondering if Rust provides any ability to add that kind of an N:M threading approach. Perhaps via an extension, macro or some other mechanism. I don't want M:N threading as Go implements it. It's a big loss of performance for marginal benefit over futures. In particular the libmill approach was tried in Rust and the results were far worse than 1:1. However, assuming this takes off I would like to see async/await…

What is the "significant performance tax" in Go? Do you have any numbers about this implementation?

When I look at the benchmark of your minihttp vs Go fasthttp, Rust is supposed to be 3x time faster without any GC and yet minihttp pulls 2M req/sec VS 1.5 for Go which is a small margin when you know how Go works and is GC.

Re: Zero-cost futures in Rust

#153
post #142

Earlier quoted context omitted.

> On Linux, you don't actually gain that much if anything over 1:1 threading. You use green threads instead of native threads because native threads have space overhead, not because they have time overhead. Attempting to spawn 100k OS threads will do strange things to most kernel scheduling algorithms; they're not optimized for that use-case.

> You use green threads instead of native threads because native threads have space overhead, not because they have time overhead. The main overhead of a thread, green or native, is the stack. The size of the stack is independent of whether you use native or green threads. Go's small stacks are actually made possible by its GC, not its choice of 1:1 or M:N. In musl, for example, you can have 2KB stacks [1] with 1:1.…

> The kernel scheduler has a much more global picture of the system compared to userland.

In most of the comparisons I've seen (usually for Erlang), worst-case latency was the important factor, so interaction with the kernel scheduler was avoided as much as possible. In the Erlang runtime, you can pass a switch to cause the userland scheduler-threads to each get bound to a particular processor core, and to cause the kernel scheduler to avoid scheduling anything on those cores. Effectively, this partitions the processor into a set of cores the OS scheduler entirely manages, and a set of cores that the userland scheduler entirely manages.

Re: Zero-cost futures in Rust

#154
post #152

Earlier quoted context omitted.

> So wondering if Rust provides any ability to add that kind of an N:M threading approach. Perhaps via an extension, macro or some other mechanism. I don't want M:N threading as Go implements it. It's a big loss of performance for marginal benefit over futures. In particular the libmill approach was tried in Rust and the results were far worse than 1:1. However, assuming this takes off I would like to see async/await…

What is the "significant performance tax" in Go? Do you have any numbers about this implementation? When I look at the benchmark of your minihttp vs Go fasthttp, Rust is supposed to be 3x time faster without any GC and yet minihttp pulls 2M req/sec VS 1.5 for Go which is a small margin when you know how Go works and is GC.

> What is the "significant performance tax" in Go?

I explain in this sibling comment: https://news.ycombinator.com/item?id=12271194

> Do you have any numbers about this implementation?

See the blog post.

Re: Zero-cost futures in Rust

#155
post #152

Earlier quoted context omitted.

What is the "significant performance tax" in Go? Do you have any numbers about this implementation? When I look at the benchmark of your minihttp vs Go fasthttp, Rust is supposed to be 3x time faster without any GC and yet minihttp pulls 2M req/sec VS 1.5 for Go which is a small margin when you know how Go works and is GC.

> What is the "significant performance tax" in Go? I explain in this sibling comment: https://news.ycombinator.com/item?id=12271194 > Do you have any numbers about this implementation? See the blog post.

I did edit my first question after seeing the numbers.

Re: Zero-cost futures in Rust

#156

Earlier quoted context omitted.

In Rust it's frequently the case that slow compilations are dominated by generating and optimizing LLVM IR. This codegen step (generating LLVM IR) often takes awhile just because we're generating so much IR. Rust takes an approach with generic functions called monomorphization which means that we generate a new version of each function for each set of generics it's instantiated with. This means that a future of a Str…

Hm. I've heard arguments that C# or Java is slow for multiple reasons, but never because of the minuscule overhead of a virtual method dispatch when using objects behind interfaces (kinds similar to trait objects). It's interesting that this is seen as significant here. Are we dealing with much shorter timescales, or just being eager to optimise everything?

Virtual dispatch per se is not terribly slow, as long as the branch is predictable by the CPU. The problem is that virtual dispatch prevents the sort of aggressive inlining and interprocedural opimizatios that C++ compilers are known to do. C# and Java JITers get around that via runtime analysis and speculative inlining, but that is done at runtime and eats away some of the precious little time available for optimisations.

Edit: spelling

Re: Zero-cost futures in Rust

#157
post #135

Earlier quoted context omitted.

I believe he meant, lots of closures. Which is not normal in C or C++, but is the normal in Rust because they are Zero-Cost. Without knowing anything about LLVM's internals, I would assume it doesn't anticipate so much closure chaining, and therefore doesn't leverage the fact that they are so easy to inline.

Lambdas in C++ are almost identical to Rust ones (each closure is a unique unnameable struct containing the captures, with an overloaded operator()), in fact, the current Rust scheme was explicitly inspired by C++11 closures. Historically (C++98), it's true that not much code used closures, because they didn't exist at a language level, but the modern language uses them much more aggressively, even pushing hard on ma…

Exactly. And hand written function objects were already common in C++98. C++1* lambdas are just (very sweet) syntactic sugar.

Re: Zero-cost futures in Rust

#158
post #155

Earlier quoted context omitted.

> What is the "significant performance tax" in Go? I explain in this sibling comment: https://news.ycombinator.com/item?id=12271194 > Do you have any numbers about this implementation? See the blog post.

I did edit my first question after seeing the numbers.

Arguing about whether a 30% difference is "significant" isn't really interesting.

Re: Zero-cost futures in Rust

#159

What's the meaning of "zero cost future" in this context? I googled the phrase and got a bunch of irrelevant material.

> C++ implementations obey the zero-overhead principle: What > you don’t use, you don’t pay for [Stroustrup, 1994]. And > further: What you do use, you couldn’t hand code any better. > > – Stroustrup So, in this context, the idea is that if you hand-rolled your own state machine, you should see no difference than using this library. And, we measured: the overhead in a benchmark comparing the two was 0.3%, that's thre…

The other concept that GP needs to understand the title is https://en.wikipedia.org/wiki/Futures_and_promises

So the title/TFA is about "futures" (in the sense of my link) that have "zero cost" (in the sense of steveklabnik's comment).

Re: Zero-cost futures in Rust

#160
post #142

Earlier quoted context omitted.

> On Linux, you don't actually gain that much if anything over 1:1 threading. You use green threads instead of native threads because native threads have space overhead, not because they have time overhead. Attempting to spawn 100k OS threads will do strange things to most kernel scheduling algorithms; they're not optimized for that use-case.

> You use green threads instead of native threads because native threads have space overhead, not because they have time overhead. The main overhead of a thread, green or native, is the stack. The size of the stack is independent of whether you use native or green threads. Go's small stacks are actually made possible by its GC, not its choice of 1:1 or M:N. In musl, for example, you can have 2KB stacks [1] with 1:1.…

I haven't seen a benchmark of huge numbers of native threads vs. a userland scheduler, but I have a hard time imagining that a userland scheduler will beat the kernel's scheduler. The kernel scheduler has a much more global picture of the system compared to userland.

Doesn't using kernel threads imply lots of context switches? Doesn't that tend to be expensive in terms of time on modern architectures?

Post reply on HN