6 ms·
This is a really common comment to see on HN threads about async/await vs fibers/virtual threads. What you're asking for is performance to be represented stati
by mike_hearn 3y ago
This is a really common comment to see on HN threads about async/await vs fibers/virtual threads.
What you're asking for is performance to be represented statically in the type system. "Blocking" is not a useful concept for this. As avodonosov is pointing out, nothing stops a syscall being incredibly fast and for a regular function that doesn't talk to the kernel at all being incredibly slow. The former won't matter for UI responsiveness, the latter will.
This isn't a theoretical concern. Historically a slow class of functions involved reading/writing to the file system, but in some cases now you'll find that doing so is basically free and you'll struggle to keep the storage device saturated without a lot of work on multi-threading. Fast NVMe SSDs like found in enterprise storage products or MacBooks are a good example of this.
There are no languages that reify performance in the type system, partly because it would mean that optimizing a function might break the callers, which doesn't make sense, and partly because the performance of a function can vary wildly depending on the parameters it's given.
Async/await is therefore basically a giant hack and confuses people about performance. It also makes maintenance difficult. If you start with a function that isn't async and suddenly realize that you need to read something from disk during its execution, even if it only happens sometimes or when the user passes in a specific option, you are forced by the runtime to mark the function async which is then viral throughout the codebase, making it a breaking change - and for what? The performance impact of reading the file could easily be lost in the noise on modern hardware and operating systems.
The right way to handle this is the Java approach (by pron, who is posting in this thread). You give the developer threads and make it cheap to have lots of them. Now break down tasks into these cheap threads and let the runtime/OS figure out if it's profitable to release the thread stack or not. They're the best placed to do it because it's a totally dynamic decision that can vary on a case-by-case basis.
- littlestymaar 3y agoYou can't encode everything about performance in the type system, but that doesn't mean you cannot do it at all: having a type system that allows you to control memory layout and allocation is what makes C++ and Rust faster than most languages. And regarding what you say about storage access: storage bandwidth is now high, but latency when accessing an SSD is still much higher than accessing RAM, and network is even worse. And it will always be the case no matter what progress hardware makes, because of the speed of light. Saying that async/await doesn't help with all performance issues is like saying Rust doesn't prevent all bugs: the statement is technically correct, but that doesn't make it interesting. > Async/await is therefore basically a giant hack and confuses people about performance. It also makes maintenance difficult. Many developers have embraced the async/await model with delight, because it instead makes maintenance easier by making the intent of the code more explicit. It's been trendy on HN to bash async/await, but you are missing the mist crucial point about software engineering: code is written for humans and is read much more than written. Async/await may be slightly more tedious to write (it's highly context dependent though, when you have concurrent tasks to execute or need cancellation, it becomes much easier with futures). > The right way to handle this is the Java approach (by pron, who is posting in this thread) No it's not, and Mr Pressler's has repeatedly shown that he misses the social and communication aspects, so it's not entirely surprising.
- ngrilly 3y ago> It's been trendy on HN to bash async/await, but you are missing the mist crucial point about software engineering: code is written for humans and is read much more than written. Readability can be in the eye of the beholder. Some of us find it easier to read concurrent code using goroutines and channels in Go, or processes and messages in Erlang, than async/await in Rust.
- littlestymaar 3y agoI don't believe you. And you know why? Because because what the async syntax permit is just a superset of the capabilities of goroutines: Any goroutine-based code can be rewritten into async/await, without changing anything of the structure: in practice all it would change is that it would add .await at yield points. Yes, async Rust code in fact resembles a lot the code you're used to, with “tasks” (which are functionally goroutines) and channels (just have a look at tokio's documentation[1] to see how it looks like). But for some situations, in addition to the goroutine style, async/await allows you to have a much more straightforward implementation (cancellation, timeout, error handling, etc.). Even then, there's nothing stopping you from using channels and select for these use-case when using Rust async/await. (In practice people don't because nobody sane would use channels and select to implement a timeout instead of tokio::timeout, but you can). Saying that goroutine and channel are easier to read than async/await Rust is like saying your car can drive slower than mine, and it just reveals that you haven't actually ever read async rust code and you're just imagining how bad it must be because of some prejudice of yours. [1]: https://tokio.rs/tokio/tutorial/channels https://tokio.rs/tokio/tutorial/channels
- ngrilly 3y agoYou're writing that "code is written for humans" (very true) and that async/await is more readable, and when you receive feedback from other humans they find async/await slightly less readable than fibers/green threads, you dismiss the feedback as unfaithful. That's quite often the problem with arguments about readability: they devolve into personal opinions. Perhaps you're right, perhaps if I'd spend much more time reading async/await code, perhaps I'll eventually find it as readable as fibers code. But that's not the case today. Others in this discussion have shared a similar feedback. To be clear, that readability "issue" is not a big deal for me, just something that I perceive as a small obstacle.
- Nullabillity 3y ago> It also makes maintenance difficult. If you start with a function that isn't async and suddenly realize that you need to read something from disk during its execution, even if it only happens sometimes or when the user passes in a specific option, you are forced by the runtime to mark the function async which is then viral throughout the codebase, making it a breaking change - and for what? The performance impact of reading the file could easily be lost in the noise on modern hardware and operating systems. You'll typically have an idea of whether or not a function performs IO from the start. Changing that after the fact violates the users' conceptual model and expectation of it, even if all existing code happens to keep working.
- mike_hearn 3y agoThere are plenty of language ecosystems where there's no particular expectations up front about whether or when a library will do IO. Consider any library that introduces some sort of config file or registry keys, or an OS where a function that was once purely in-process is extracted to a sandboxed extra process.
- Nullabillity 3y ago> There are plenty of language ecosystems where there's no particular expectations up front about whether or when a library will do IO. There are languages that don't enforce the expectation on a type level, but that doesn't mean that people don't have expectations. > Consider any library that introduces some sort of config file or registry keys Yeah, please don't do this behind my back. Load during init, and ask for permission first (by making me call something like Config::load() if I want to respect it). > or an OS where a function that was once purely in-process is extracted to a sandboxed extra process. Slightly more reasonable, but this still introduces a lot of considerations that the application developer needs to be aware of (how should the library find its helper binary? what if the sandboxing mechanism fails or isn't available?).
- mike_hearn 3y agoFor the sandbox example I was thinking of desktop operating systems where things like file IO can become brokered without apps being aware of it. So the API doesn't change, but the implementation introduces IPC where previously there wasn't any. In practice it works fine.
- cryptonector 3y ago> There are no languages that reify performance in the type system, Async/await is a way of partially doing... just that, but without a having to indicate what is "blocking", and if an async function blocks, well, you'll be unhappy, so don't do that. For a great deal of things this is plenty good enough, but from a computer science perspective it's deeply unsatisfying because one would want the type system to prevent making such mistakes. At least with async/await an executor could start more threads when an async thread makes a known-blocking call, thus putting a band-aid on the problem. Perhaps the compiler could reason about the complexity of code (e.g., recursion and nested loops w/ large or unclear bounds -> accidentally quadratic -> slow -> "blocking") and decide if a pure function is "blocking" by dint of being slow. File I/O libraries could check if the underlying devices are local and fast vs. remote and slow, and then file I/O could always be async but completing before returning when the I/O is thought to be fast. This all feels likely to cause more problems than it solves. If green threads turn out not to be good enough then it's easier to accept the async/await compromise and reason correctly about blocking vs. not.
- mike_hearn 3y agoA function being marked async tells you nothing in particular about the performance of that operation. It could be anything from seconds to milliseconds and routinely is e.g. elliptic curve cryptography operations that are plenty fast enough to execute on a UI thread animating at 60fps are nonetheless marked async on the web, whilst attaching a giant document fragment to the live DOM - which might trigger very intensive rerendering calculations - isn't. Nothing stops you from starting more threads when you run low in a scenario when there are only threads also. That's how the JVM ForkJoinPool works. If your threads end up all blocked, more are started automatically.