Live data from Hacker News

Zero-cost futures in Rust

aturon.github.io

221–230 of 348 posts

Re: Zero-cost futures in Rust

#221
post #122

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…

> I don't want M:N threading as Go implements it. As someone who writes code for enterpricey businesses doing a lot of I/O bound stuff, golang style M:N threading is a godsend over Java's standard library, and other common platforms in that space. Being able to express your code in a sequential manor and still gain the performance offered by implicit fiber,goroutine,w/e scheduling is pretty awesome. With futures, the…

> I can still switch to C for specific use cases.

So use go and switch to rust for specific use cases.

Re: Zero-cost futures in Rust

#222

Earlier quoted context omitted.

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?

> Doesn't using kernel threads imply lots of context switches? No, it's actually fewer context switches. That's because each I/O completion event goes straight from the kernel to the code that was waiting on it (1 context switch), not from the kernel to the userland dispatcher to the code that was waiting on it (2 context switches).

This is not quite right. The userland dispatcher to run next thread is not a context switch: the TLB doesn't need to get flushed for instance. Furthermore many userland threads can be woken at once, and don't incur context switches when they suspend, prompting the next runnable thread to run.

If you're willing to put in a lot of compiler work userland thread switching is a function call, literally. I don't 1:1 threads with small stack are going to compete with that.

Re: Zero-cost futures in Rust

#223

Earlier quoted context omitted.

You don't have to destroy the "stack frame" (by which I assume you mean the state retained between blocking operations) every time you enter and leave. Why do you think you need to?

He's talking about the difference between swapping the stack pointer and executing a single "jump" instruction vs initiating a full function call (pushing argument(s), function prologue/epilogue)

Calling a function isn't "allocation". And you don't just swap the stack pointer: you have to reload the register state. It's the same cost as a function call.

Re: Zero-cost futures in Rust

#224

Earlier quoted context omitted.

> Well, one advantage the Go runtime has is the green threads are cooperatively scheduled, so switches are a lot more lightweight. Switches occur on timeouts (which involve a round trip through the kernel), on I/O events (which also involve a round trip through the kernel), or on goroutine message passing. So in every case except goroutine message sends, the switches don't actually save you a trip through the kernel.

System call context switching is cheaper than and different from thread/scheduler context switching.

OK, but that's irrelevant in this context because Go has to context switch either way.

Re: Zero-cost futures in Rust

#225

Earlier quoted context omitted.

> no sane way to intermix them. My understanding is, the idea is to put the blocking stuff in a threadpool with https://github.com/alexcrichton/futures-rs/tree/master/futur...

Right, there may be a lot of back and forth marshaling between using thread pools and not depending on whether the library you're using is futures based or not. Maybe you use one library that is futures-based and one that isn't. Maybe the library you use is mostly non-blocking except for one use of sleep() or another esotorically blocking call. It's just annoying and prone to error. Most people may not even be aware…

> Right, there may be a lot of back and forth marshaling between using thread pools and not depending on whether the library you're using is futures based or not.

So just like if you use cgo. You can't get away from having to deal with the issue entirely; the most you can do is to punt it to the FFI layer. There is the question of how much of the community is using blocking vs. nonblocking I/O, to be sure, but Go has a version of that too: how much of the community is using cgo vs. how much of the community is writing in pure Go.

> This is what I mean by having two different languages.

Calling them "two different languages" is a huge exaggeration. You simply block or switch to a thread pool: it's very easy.

> I suspect most programmers will be loathe to have to deal with the extra mental tax.

I like having the low-level control over blocking vs. not, especially in situations where I can't use async I/O everywhere (for example, my work on Servo). In fact, it's essential.

Ultimately this is going to come down to "you should be willing to pay a performance and control tax for a more ergonomic model" vs. "you shouldn't give up performance and control for a small amount of ergonomics". Yes, there is a tradeoff here. That's fine. Taking Go's side of the tradeoff would make Rust unusable for my domain, and for many others (which is why M:N was the most controversial issue ever in the Rust community, with most of the community demanding it to be removed, while in Go nobody questions it). Some people may not want Rust's side of the tradeoff, and that's fine too.

Re: Zero-cost futures in Rust

#226

Earlier quoted context omitted.

Are libmill and Go's approach the same? I.e. Holding concurrent state in multiple stack frames? What causes that to have degraded performance? Is this new approach different because instead of stack frames you just have dynamically allocated callback state?

> Are libmill and Go's approach the same? I.e. Holding concurrent state in multiple stack frames? What causes that to have degraded performance? In the M:N approach you have to allocate stack space for each goroutine that you spawn. This requires that you either know the size of the stack up front (generally not possible without being conservative and requesting a large allocation) or that you start small and grow (r…

> This requires that you either know the size of the stack up front ... generally not possible without being conservative

Well, you are writing the compiler. Sure you'd be up against the halting problem, but relatively few functions are (non-tail) recursive.

Perhaps the unwieldiness of a large stack is better attributed to the feature of unbounded recursion (and FFI into "uncharted territory") than the feature of green threads.

I appreciate that Rust has already been down the road of lightweight threads. This statement just struck me as an assumption that deserved to be questioned.

Re: Zero-cost futures in Rust

#227
post #219

Earlier quoted context omitted.

> (Unfortunately, the default has to be the opposite in browser-based languages due to performance concerns.) Also in Rust. Most apps that aren't servers don't want async I/O, and it causes a lot of problems when you need high-performance FFI. For example, in Servo async-everywhere would be a big problem for WebRender, which needs to be able to call OpenGL extremely quickly. > Defaults matter. If some people use asyn…

Easy and error prone. Sometimes libraries pretend to be async and accidentally are sync. Consider some library that in the normal case just does some pure computation, but logs to syslog or something on some error condition. If you use that library in an async context, it could work fine most of the time, until you hit some unexpected situation where it happens to make a network request to some syslog daemon and bloc…

> It's also the case that often async libraries depend on some sync library and so they have their own worker pool. You can easily have many libraries with their own worker pools all using more resources then they need.

The Rust story will not be complete without a canonical single implementation of a thread pool that everybody doing async I/O uses for blocking tasks.

> For example, if you have some async worker that delegates some sync task to the worker pool, and that sync task happens to use some async function and blocks on it, and that async function ALSO has a sync task and attempts to delegate it to a worker pool, and that worker pool is bounded

I think the solution here is "don't have strictly bounded worker pools". This is what cgo does, I believe.

> It's non-trivial to provide a good debugging experience that doesn't cause extra load in production environments.

But this is the exact same problem that any M:N system will have. So I don't see any special problem for Rust's system here.

Re: Zero-cost futures in Rust

#228

Earlier quoted context omitted.

> (Unfortunately, the default has to be the opposite in browser-based languages due to performance concerns.) Also in Rust. Most apps that aren't servers don't want async I/O, and it causes a lot of problems when you need high-performance FFI. For example, in Servo async-everywhere would be a big problem for WebRender, which needs to be able to call OpenGL extremely quickly. > Defaults matter. If some people use asyn…

> Also in Rust. Most apps that aren't servers don't want async I/O, and it causes a lot of problems when you need high-performance FFI. For example, in Servo async-everywhere would be a big problem for WebRender, which needs to be able to call OpenGL extremely quickly. I don't understand why this is the case. Since async/await allows the compiler to transform the code into a state machine, why would it be not be able…

Because async-everywhere usually means pushing blocking FFI calls over to a thread pool, which would be unacceptably slow for e.g. OpenGL.

Re: Zero-cost futures in Rust

#229
post #212

Earlier quoted context omitted.

> Well, one advantage the Go runtime has is the green threads are cooperatively scheduled, so switches are a lot more lightweight. Switches occur on timeouts (which involve a round trip through the kernel), on I/O events (which also involve a round trip through the kernel), or on goroutine message passing. So in every case except goroutine message sends, the switches don't actually save you a trip through the kernel.

Couldn't you switch on timeout using some kind of CPU counter, e.g. RDTSC?

Then you're polling the system clock all the time, which is a large unnecessary throughput loss.

Re: Zero-cost futures in Rust

#230

Earlier quoted context omitted.

> Are libmill and Go's approach the same? I.e. Holding concurrent state in multiple stack frames? What causes that to have degraded performance? In the M:N approach you have to allocate stack space for each goroutine that you spawn. This requires that you either know the size of the stack up front (generally not possible without being conservative and requesting a large allocation) or that you start small and grow (r…

> This requires that you either know the size of the stack up front ... generally not possible without being conservative Well, you are writing the compiler. Sure you'd be up against the halting problem, but relatively few functions are (non-tail) recursive. Perhaps the unwieldiness of a large stack is better attributed to the feature of unbounded recursion (and FFI into "uncharted territory") than the feature of gre…

It would require higher order control flow analysis like k-CFA, which would certainly fail to produce a bounded stack size on any nontrivial program.

The futures library is the control flow analysis. Because it uses the type system instead of higher order control flow analysis, it actually achieves precision.

Post reply on HN