Live data from Hacker News

Zero-cost futures in Rust

aturon.github.io

331–340 of 348 posts

Re: Zero-cost futures in Rust

#331

Earlier quoted context omitted.

Yes, but indirectly means "&Fn", but not "&F where F: Fn". And a whole program doesn't need to conform, only individual threads. Presumably the ones you want to make a lot of. (And if a little dynamicism was required, its expense could be paid for at the use, by creating a fresh necessary-sized stack at that point. But I'm probably opening up old split-stack wounds, sorry)

The problem with an optimization like this is that the current approach always gives you a guarantee on runtime cost. Heuristic based optimizations make performance harder to ensure, not to mention recovering performance when you fall outside the valid subset much more difficult. You can obviously provide another static analysis to warn you about violations, but this seems is more complex, and less flexible then the…

Actually the "current approach" gives you no bound on the stack size. You guess, hope, and test (or do this analysis manually).

While your general objection is good to always keep in mind, it is a tradeoff. There is no perfect solution when you're up against the halting problem (unless you're proposing to change the language to only bounded recursion).

What I'm arguing for here is for more than the single problem the OP is solving, so it's not a case of choosing one or the other and calling it a day.

Re: Zero-cost futures in Rust

#332
post #76

I'm rather surprised by the benchmark; I would expect the Go benchmark to be faster than Java (and the fact that it isn't may indicate some improvements that can be done to fasthttp by learning from rapidoid or minihttp). Then again, the difference isn't that much, so it just could be implementation details that would require a total refactor to fix.

You may find this makes somewhere more sense to think of it as ~5.3 microseconds per request for fasthttp vs. ~4.8 microseconds for Java vs. ~4.3 for Rust. It's 40 microseconds or so for the standard lib Go. I'm just eyeballing the graph but this should be close enough (dominated by local CPU variances and such). Just as some people point out that "gallons per mile" is a more intuitively useful way of thinking, I thi…

In the case of valaya/fasthttp vs. net/http, a bonus thing is that with fasthttp you (currently+AFAIK) lose HTTP/2 support! Needs vary a ton with the app, environment, etc., of course, but it'd be sad to change your server to save 40µs CPU per req. then wind up making users wait extra RTTs--tens to hundreds of ms--as a result.

Re: Zero-cost futures in Rust

#333

Earlier quoted context omitted.

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

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

You can actually do a pretty good job without k-CFA.

First, and i know you know this, when you say "fail to produce a bounded stack size", you really mean "a reasonable bounded stack size".

A bounded stack size of easy, and does not require context sensitive analysis: The stack size is just max sum of stack sizes of of meet over all paths in an acyclic graph :)

The cycles, you either can statically calculate the recurrence count or you can't.

Bounded heap size is pretty much impossible, but stack size is pretty easy.

Recursion is also not hard. You form strongly connected components. You try to prove how often the component is cycled. If you can, victory. If you can't, you can bound it unless it's truly dynamic.

More to the point, here's a paper on doing it with ADA in GCC in 2009 (IE with sticks and fire):

http://www.adacore.com/uploads/technical-papers/Stack_Analys...

Note they are within 2% on most cases, and can detect whether it's statically knowable, bounded, or dynamically changing.

Re: Zero-cost futures in Rust

#334
post #260

Earlier quoted context omitted.

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

Is this something Erlang supports, or is it something that you get merely because you've pinned threads to a specific core? I know with cgroups in Linux you can pin processes to cores pretty easily. Just curious how Erlang makes this easier.

If you mean that in terms of "can you ask Erlang itself to do this for you", then yes: http://erlang.org/doc/man/erl.html#+sbt

If you mean that in terms of "does the Erlang runtime intelligently take advantage of the fact that its schedulers are pinned to cores to do things you don't get from plain OS-level pinning", I'm not sure.

I think it might, though. This is my impression from reading, a year or so back, the same docs I just linked; you can read them for yourself and form your own opinion if you think this sounds crazy:

It seems like ERTS (the Erlang runtime: BEAM VM + associated processes like epmd and heart) has a pool of "async IO" threads, separate from the regular scheduler threads, that just get blocking syscalls scheduled onto them. Erlang will, if-and-only-if it knows it has pinned schedulers, attempt to "pair" async IO threads with scheduler threads, so that Erlang processes that cause syscalls schedule those syscalls onto "their" async IO threads, and the completion events can go directly back to the scheduler-thread that should contain the Erlang process that wants to unblock in response to them.†

In the default case, if you don't tell ERTS any different, it'll assume you've got one (UMA) CPU with N cores, and will try to pin async IO threads to the same cores as their paired scheduler-threads. This has context-switching overhead, but not much, since 1. the async IO thread is mostly doing kernel select() polling and racing to sleep, and 2. the two threads are in a producer-consumer relationship, like a Unix pipeline, where both can progress independently without needing to synchronize.

If you want, though, you can further optimize by feeding ERTS a CPU map, describing how the cores in your machine are grouped into CPU packages, and how the CPU packages are further grouped into NUMA memory-access groups. ERTS will then attempt to schedule its async IO threads onto a separate core of the same CPU package, or if not possible, the same NUMA group* as the scheduler-thread, to decrease IPC memory-barrier flushing overhead. (The IPC message is still forced to dump from a given core's cache-lines into the CPU or to NUMA local memory, but it doesn't have to go all the way to main memory.)

ERTS will also, when fed a CPU map, penalize the choice in its scheduling algorithm to move an Erlang process to a different CPU package or NUMA group. (It will still do it, but only if it has no other choice.)

---

† This is in contrast to a runtime without "native" green-threads, like the JVM, where even if you've got an async IO pool, it just sees an opaque pool of runtime threads and sends its completion events to one at random, and then it's the job of a framework like Quasar to take time out of the job of each of its JVM runtime threads to catch those messages and route them to a scheduler running on one of said runtime threads.

The same is true of an HVM hypervisor: without both OS support (paravirtualization) plus core pinning inside each VM, a hardware interrupt will just "arrive at" the same pCPU that asked to be interrupted, even if the vCPU that was scheduled on that pCPU when it made the hypercall is now somewhere else. This is why SR-IOV is so important: it effectively gives VMs their own named channels for hardware to address messages to, so they don't get delayed by misdelivery.

Re: Zero-cost futures in Rust

#335
post #254

Earlier quoted context omitted.

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.

Given the kind of "cloud"-backed startups most of HN are working on, I think the more practical measurement would be the overhead of a ring 0/3 separation in a VM, plus hypercalls, plus a ring 0/3 separation in the hypervisor (for paravirtualized syscalls); averaged against a ring 0/3 separation in a VM, plus SR-IOV virtualization (for HVM-backed syscalls.)

Yeah, this doesn't apply for context-switches that happen because of plain old pre-emption, but it does happen if the context-switch is because e.g. a network packet wants to arrive at another process in your VM than the one that's currently running.

I would imagine that there's a reason bare-metal IaaS providers have a business model. :)

Re: Zero-cost futures in Rust

#336

Earlier quoted context omitted.

> although i think the approach used by C++ coroutine is better How? > The main advantage of the go model is that both asynchronous and synchronous operations are identical, with async/await you still need to model the async operation and the sync operation with different types. It's more like "everything is synchronous" in the Go model. Semantically, Go doesn't have async I/O at all. It has a userspace M:N implement…

> How ? In term of allocation : When the future uses some variable present on the current function stack you have two options 1 - Waiting for the future to complete before exiting the current function (which essentially is blocking) 2 - Allocating the closure on th heap (allocation + deallocation) In a language with coroutine support , we have a third alternative. Instead of block or allocating memory, it's possible…

I just watched (mostly) the CppCon talk you posted elsewhere. The coroutine approach is really interesting, but I'm confused as to how it's different. According to a source I found[1], the way coroutines are implemented is that a new stack is created on the heap and it moves back and forth between that. Isn't that the same case here? Is the compiler level implementation(as opposed to boost, as in the linked reference) different in some way?

1: http://stackoverflow.com/questions/121757/how-do-you-impleme...

Re: Zero-cost futures in Rust

#337

I'm confused by .map(|row| { json::encode(row) }) .map(|val| some_new_value(val)) Over .map(json::encode) .map(some_new_value) Is the explicit extra layer of lambda generally prefered in Rust over just passing the functions?

I find point-free style confusing sometimes. This is because I can't tell what is being encoded. Then in the first example I would be like "OK, we encode a row..."

Re: Zero-cost futures in Rust

#338

Earlier quoted context omitted.

I think there's little or no evidence that "you can recover most of the M:N ergonomics over time via async/await style syntax" , despite a decade or so of attempts. I think there's an underlying semantic concern that seems unsugarable. Munificent's http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y... expresses the problem eloquently. None of that means you're wrong about async making "a lot more sense for…

This is currently hotly debated in the C++ committee. Some people want shallow C#, python style generators, while other want proper stackful coroutines a-la Lua (full disclosure: I'm on this group). A third group is trying to mediate and trying to come up with an hybrid stackful model that can be optimized as well the async/await model at least in some cases (I.e. full cps transform and fallback to a cactus stack whe…

I've been writing async I/O networking software for about 15 years now. Early on most of that was in C, now it's split about 50/50 between C and Lua. Most of my C I/O code is still in C because I prefer my libraries to be reuseable outside of Lua or any particular event loop, and they often are. Lua's coroutines are usually higher up the stack, juggling more abstract state; and I use them for more than asynchronous I/O or asynchronous tasks.

The thing about async/await is that in a language like C, I can already accomplish much of that with tricks like Duff's Device and macros. It has its limitations, but IME they're not much more onerous than the limitations of async/await, especially in the context of a language lacking GC. I have to manually keep state off the stack (or otherwise copy/restore it), but you do that anyhow when you don't have lexical closures and GC, and often even when you do.

The beautiful thing about coroutines in Lua is that it's based on a threading model, but not one bound to the C stack or a kernel thread, which are completely orthogonal concerns left to the application to deal with or not deal with. And it does this while preserving functions as first-class objects. Neither callers nor callees need to know anything about coroutines. That kind of composability makes coroutines useful and convenient for many more things than simulating green threading or managing CPU parallelism. Among other things, it means I can mix-and-match functional and imperative styles according to the problem, and not whether it will be convenient to then make use of coroutines. It means that I have a single, natural call stack--not an implicit stack and an explicit stack. async/await and futures unify your data stack, but you're still manually managing the call stack through syntactic devices or otherwise formalized calling conventions. However heavily sugared, it will hinder the design of your software no less than if you had to manually manage the data stack, too.

Coroutines that aren't stackful aren't nearly as powerful in terms of problem solving. Without them being stackful, it's a horribly leaky abstraction for non-trivial uses. Most people would agree that the C preprocessor is a mess, and that functions as first-class objects are powerful. So modern languages strive to create templating systems that allow you to construct _real_ functions that are indistinguishable from any another function. But then they introduce monstrosities like futures or async/await, that beautiful symmetry is broken. It's like bringing back C's macro preprocessor--now you have regular functions and these weird things with different syntactic and control follow semantics, whether you wanted it or not. The decision is no longer yours, which means you're bending to the language's deficiencies.

Why even bother with such half-baked solutions? In almost every case it's utterly transparent that these solutions exist for the benefit of the compiler and runtime author, usually because of intentional or unintentional technical debt--a direct or indirect dependency on the C or kernel stack. For C++ it's understandably a difficult dilemma, but for every other language it's a total cop-out.

Then these solutions are sold to the public by prettifying the implementations with fancy terminology and beguiling examples showing how they can be used to implement async I/O or parallel CPU jobs. But few, if any, language features are so narrowly tailored to such specific use cases. Why? Because languages are supposed to provide simple building blocks that compose as seamlessly as possible at a much higher level of abstraction than, e.g., a slightly nicer way to implement HTTP long-polling servers. Such contrivances are as removed from the basic simplicity of the function as C's macros are from first-class functions. In both cases you can implement solutions for a certain subset of problems that superficially look convenient and nice; but in the real world the limitations become swiftly apparent, and you realize a lot of effort was spent in design and implementation for little real-world gain.

With Lua's coroutines, I can implement a futures pattern easily when it's appropriate, and it will be more powerful because the futures themselves can make use of coroutines both internally and externally. But in my use of coroutines in Lua futures are rarely the most natural design pattern. Sometime you want a full-blown CPS solution, sometimes you simply want to be able to arbitrarily swap producer/consumer control flow, for example in a lexer. Often you want a mixture of all of these. Coroutines--stackful coroutines--provide all that and more, seamlessly.

Futures only look nice and elegant in contrast to event loop oriented, callback-style programming. But that's a really, really low bar. Please aim higher, people!

Re: Zero-cost futures in Rust

#340
post #339

Earlier quoted context omitted.

Nothing special yet; those are still implemented in a blocking way, and so should be put into a threadpool.

A threadpool with futures support, I gather. Very nice.

https://github.com/alexcrichton/futures-rs/tree/master/futur...
Post reply on HN