Live data from Hacker News

Zero-cost futures in Rust

aturon.github.io

251–260 of 348 posts

Re: Zero-cost futures in Rust

#251

This is cute. This is clever. Whether or not it's too clever time will tell. A year ago, I noted that Rust was starting out at roughly the cruft level C++ took 20 years to reach. Rust is now well beyond that. All this "futures" stuff strongly favors the main path over any other paths. You can't loop, retry, or easily branch on an error, other than bailing out. It's really a weird syntax for describing a limited type…

> All this "futures" stuff strongly favors the main path over any other paths. You can't loop, retry, or easily branch on an error, other than bailing out. You can do all of that.

OK, make an HTTP request, and if it fails, wait 2 seconds and retry. After 10 times, give up.

Re: Zero-cost futures in Rust

#252
post #243

Earlier quoted context omitted.

For the example if you are using kernel bypass of the network stack, which is increasingly common in high throughput/low latency applications.

If you're going to bypass the kernel network stack for performance, you're definitely not going to write the rest of your code in go. The garbage collector, goroutine scheduler, and conservative compile time optimizations will totally undermine the end goal. If you're in that world, you're using C, C++, or That if you're feeling dangerous.

I've done kernel bypass work on the JVM.

Re: Zero-cost futures in Rust

#253

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?

A tremendous amount of effort has gone into the CLR towards optimizing interface dispatch, because at one time it was slow. Interface dispatches are cached at the call site to avoid real virtual (vtable) dispatch, just like a Smalltalk or JavaScript VM would.

Re: Zero-cost futures in Rust

#254

Earlier quoted context omitted.

> 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).

The real cost of a true "context switch" is the transition from user level to kernel level, which takes thousands of cycles. This cost isn't incurred on a userland context switch, so those costs aren't comparable.

Thousands of cycles for a SYSCALL/SYSRET on a reasonably modern Intel/AMD CPU? I think you should try to measure that.

Re: Zero-cost futures in Rust

#255

Earlier quoted context omitted.

> You can recover most of the M:N ergonomics over time via async/await style syntax. While i agree with the tradeoff made by rust (although i think the approach used by C++ coroutine is better), i don't think that having async/await syntax give you "most" of the ergonomics of the Go M:N model . The main advantage of the go model is that both asynchronous and synchronous operations are identical, with async/await you…

> The main advantage of the go model is that both asynchronous and synchronous operations are identical There is a bit of misunderstanding on your part. There are no asynchronous operations in that go model, everything is synchronous. There is no event loop underneath, despite what some people claim. And this is absolutely not an advantage in a shared memory environment. Instead it forces you to do synchronization to…

> There are no asynchronous operations in that go model, everything is synchronous.

You are the second person saying this, and i have to admit i am really confused by this statement. From my understanding, golang does not expose an asyncio interface, but this doesn't meant that golang runtime doesnt perform io operations asynchronously. So golang expose async operation through a synchronious interface, which is what most language construct (C# async/await, F# async monad etc..) try to emulate.

From https://morsmachine.dk/go-scheduler , when a goroutine performs a blocking operation, the local scheduler the remaining goroutine are migrated to another OS thread, allowing them to continue their execution while the blocking operation completes asynchroniously.

Re: Zero-cost futures in Rust

#256

Earlier quoted context omitted.

How does futures count as cruft? It's a pure library. This is like saying GObject is cruft in C. It is a library that some folks don't like (like most libraries), but it is not part of the language and nobody is forced to use it in their code. Why do you think Rust has cruft anyway? It has a lot of typesystem features, yes, but this is no different from languages like Haskell. These features work together nicely and…

Yes, it's a pure library. But this sort of thing is becoming standard for Rust. If your code isn't full of "foo.and_then(|x| ...)" it's uncool. This isn't "functional"; these functions have major side effects. The bothersome thing is that the control structures of the language are hidden under object-specific functions. "Things really start getting interesting with futures when you combine them. There are endless way…

Your definition of cruft is rather strange. Yes, Rust code can be kind of complicated and messy-looking, but it makes sense when you think about it. All the parts justify their existence. IMO, cruft is stuff that doesn't make sense or is extraneous; incidental rather than essential complexity, in general.

Re: Zero-cost futures in Rust

#257

Earlier quoted context omitted.

Not exactly. A given type can have infinite implementations of iterator (by implementing for different A) but only one impl for iterator

I don't think I get that without an example. Do you know of any simple examples?

Generic traits can be implemented multiple times, as long as the params differ. This is generally used to establish relationships between types, which can naturally be M:N. Comparison and conversion are standard use cases:

    impl PartialOrd for OsString
    impl PartialOrd for OsString
    impl PartialOrd for OsString
    impl PartialOrd> for OsString
Traits without generics can only be implemented once (because there's no generic params to "differentiate" impls). But it's often still desirable/necessary to relate types to the implementation. This is much more common, as it's generally a case that some type is a ThingDoer in only one way (and having it be a ThingDoer in more than one way may be confusing).

Associated types are often more convenient and give the compiler more room to reason about your code.

Re: Zero-cost futures in Rust

#258

Earlier quoted context omitted.

How does futures count as cruft? It's a pure library. This is like saying GObject is cruft in C. It is a library that some folks don't like (like most libraries), but it is not part of the language and nobody is forced to use it in their code. Why do you think Rust has cruft anyway? It has a lot of typesystem features, yes, but this is no different from languages like Haskell. These features work together nicely and…

Yes, it's a pure library. But this sort of thing is becoming standard for Rust. If your code isn't full of "foo.and_then(|x| ...)" it's uncool. This isn't "functional"; these functions have major side effects. The bothersome thing is that the control structures of the language are hidden under object-specific functions. "Things really start getting interesting with futures when you combine them. There are endless way…

> This isn't "functional"; these functions have major side effects

uh, no, I've rarely seen adaptors like and_then being used with side effects (folks use regular loops if they want that). Rust doesn't have a strict notion of purity, but that doesn't mean that most rust code isn't pure.

There's nothing wrong with having lots of adaptors scattered around the code, either. It's not less readable, it's just different.

> There are endless ways of doing so." I'd rather have "there's only one way to do it", as in Python

Uh, "there are endless ways of combining future adaptors", not "there are endless ways of solving a problem". Each combination of adaptors solves a different problem (mostly).

> That usually leads to code that's hard to read and debug

This is async code. This has always been hard to read and debug. Futures make the control flow more explicit, if anything (especially if you have async and await), because the flow is now in one place, at least. Grokking manual event loop code is much more annoying.

Sure, the async argument doesn't apply to regular iterators and Option. However, these "object specific functions" are not object specific. All futures have the same adaptors. All iterators have the same adaptors. The only special objects with their own set of such methods are Result and Option. These share the names of the methods, and these are used often enough to justify it. There aren't that many of them either, so this really isn't that big a deal. You just need to know what each of this small number of methods does. It only hides control flow if you're not aware of these methods, which is a state of mind that goes away quickly after the first few rust programs.

Besides, because of the closures it's pretty obvious that some tweaking of control flow is happening, so it isn't hidden. You can check the docs for that function to know exactly what the tweaking is.

I also don't know what you mean by "traditional code", this pattern is exceedingly common in languages which aren't C or C++.

Re: Zero-cost futures in Rust

#259

Big downside is now you will have a dichotomy of functions that block using futures and functions that block at the OS level and no sane way to intermix them. Rust essentially becomes two languages. Async/await sugar doesn't fix this. Would be great if functions could be written in a general way for both IO models and users could select the implementation at their convenience.

> Would be great if functions could be written in a general way for both IO models and users could select the implementation at their convenience. We tried this with a compile-time switch between 1:1 and M:N threading in earlier versions of Rust and the results pleased nobody. It was slow, complex, and unwieldy.

It's my understanding that the alternative green thread runtime used stack swapping.

What I'm referring to here is the same futures method under the hood but transparent to the user.

It's also my understanding that there were unrelated engineering constraints that caused it to be unwieldy, such as binary size. I believe it's possible to provide an alternative runtime without it necessarily affecting the main configuration or limited environments like embedded devices.

Re: Zero-cost futures in Rust

#260
post #153

Earlier quoted context omitted.

> 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…

If the kernel scheduler completely hands control of the core over to the program, doesn't that mean you can only run one or two programs on the entire machine at once without running out of cores? Surely the kernel still schedules other threads on that core too.

Yes, that's rather the point: we're talking about highly-multicore server machines (e.g. 16/32 cores, or perhaps far more) entirely dedicated to running your extremely-concurrent application. You want all but one or two of those cores just running the app and nothing else. You leave one or two cores for the "control plane" or "supervisor"—the OS—to schedule all the rest of its tasks on.

It's a lot like a machine running a hypervisor with a single VM on it set to consume 100% of available resources—but actually slightly more efficient than that, since a guest can intelligently pin allocate cores to itself and pin its scheduler-threads to them and then just stop thinking about the pinning, while a hypervisor-host is stuck constantly thinking about whether its vCPUs-to-pCPU mapping is currently optimal, with what's basically a black box consuming those vCPUs.

Post reply on HN