A meaningless comparison. Linux, being a preemptively multitasking OS, switches thread contexts regardless of what you're running. So the Rust async context switch is on top of the regular Linux context switch, not instead .
When you're in sub microsecond time scales, preemption events are relatively rare.
Comparison of Rust async and Linux thread context switch time and memory use
111–120 of 201 posts
Re: Comparison of Rust async and Linux thread context switch time and memory use
#112Performance profiling on a laptop is largely pointless. There's too much stuff trying to conserve power by limiting performance.
I have done benchmarking on a linux laptop. Once you disable turbo-boost you get quite consistent results for CPU-bound tasks at least.
Re: Comparison of Rust async and Linux thread context switch time and memory use
#113Quickest summary: Rust async is >3x faster and lighter than Linux threads. This is a great accomplishment for Rust.
Keep in mind that a new async task doesn't create a new thread. So yes, "not creating a new thread" is 3x faster than "creating a thread". If the app layer can context switch using language level constructs, and do co-operative switching, then yes, one gets the 3x benefit. imho, whether the async executor and scheduler is performant enough to manage the tasks is what one should worry about.
The benchmark compares fibers to threads and has little to do with Rust. You will see the same numbers for a fibers implementation in any natively compiled language like C or Java.
The title is completely misleading, especially for most people who are not aware of this important distinction.
Re: Comparison of Rust async and Linux thread context switch time and memory use
#114Earlier quoted context omitted.
> io_uring's "slow" path is roughly one syscall per iteration, right? Right, so if a blocking API makes 1 syscall, io_uring would make N syscals for N iterations.
Where 1 iteration and 1 syscall to io_uring_enter() is submitting 100s of I/O operations per io_uring_enter() syscall (and you can even run the ring buffers with the kernel set to poll so you can do 0 syscalls if that's not already enough). That's pretty huge amortization. Rough benchmarks we've done are showing double throughput for io_uring for 4096 byte AF sector write/fsync/read combos: https://github.com/coilhq/…
Same for the blocking case. If I do a syscall to read a whole file, its just 1 syscall creating millions of I/O operations.
Re: Comparison of Rust async and Linux thread context switch time and memory use
#115Earlier quoted context omitted.
Rust's async model takes very little time to grasp. It's very explicit. Nothing runs in the background (contrast that with NodeJS). You have strong static types to help you to know when you got a Future, you can decide where/when to await it. It's programming with threads, where you have a thread pool and pipes to put tasks onto that and a helper function/macro. Await does this under the hood of course.
That is until you get a weird error or try to do something more complicated.
Also I agree that multi-threaded Rust is probably the best alternative to async Rust.
[0] I have no problems with the GIL, but it's yet another factor to consider when a python multi-threaded program stops working as intended
Re: Comparison of Rust async and Linux thread context switch time and memory use
#116The problem with threads is you need to correctly size your thread pool. That's hugely difficult if you have unknown lengths of blocking IO.
Re: Comparison of Rust async and Linux thread context switch time and memory use
#117Earlier quoted context omitted.
Btw, have you read this: https://async.rs/blog/stop-worrying-about-blocking-the-new-a... async-std allows to run blocking calls without hoops rather efficiently: async fn read_to_string(path: impl AsRef ) -> io::Result { std::fs::read_to_string(path) } It doesn't have await inside! My mind was blown as I saw that.
> should a task execute for too long, the runtime will automatically react by spawning a new executor thread taking over the current thread’s work. That is a super interesting strategy, though obviously only works when you can « afford » a multithreaded scheduler. Anyway I wonder how they manage this, signals?
Yeah, for example in comparison actix-web only uses single threaded workers - one per core. Future in actix-web doesn’t have to be Send or Sync, and I think it’s incompatible with what async-std is doing here. That design is almost certainly one of the reasons actix-web tops phoronix
Re: Comparison of Rust async and Linux thread context switch time and memory use
#118Earlier quoted context omitted.
I’d happily take an async-by-default world over a world where some APIs only exist through blocking calls. A classically threaded program can easily block on a future, but wrapping a blocking call in an otherwise asynchronous program is complicated, expensive and error prone work.
> A classically threaded program can easily block on a future, but wrapping a blocking call in an otherwise asynchronous program is complicated, expensive and error prone work. It’s really not though, at least as long as the parameters and results are Send. For instance Tokio has a spawn_blocking which runs the function on one of the blocking threads it spawns on-demand specifically for that use. Meanwhile « blocking…
Re: Comparison of Rust async and Linux thread context switch time and memory use
#119> People often see that there's some theoretical benefit of async and then they accept far less ergonomic coding styles and the additional bug classes that only happen on async due to accidental blocking etc... despite the fact that when you consider a real-world deployed application, those "benefits" become indistinguishable from noise. However, due to the additional bug classes and worse ergonomics, there is now le…
Speaking of futures_unordered and similar patterns, I think a part of the "async promise" that has failed is the lack of concurrency for a single user request by default in most languages. That is, the 'easy' path is to write code such as the following (in vaguely C# pseudocode): var p = await GetUserPermission( username ); var c = await GetServerConfig(); var m = await GetMessageOfTheDay(); Assume each await call is…
let p, c, m = join!(
GetUserPermission(username),
GetServerConfig(),
GetMessageOftheDay()
);
(you don't even have to write await when using the join macro)With such simple syntax available it seems obvious to me that one would want to use it as often as possible, and it's also much simpler (and probably cheaper) than dispatching those three tasks to a thread pool.
Re: Comparison of Rust async and Linux thread context switch time and memory use
#120> People often see that there's some theoretical benefit of async and then they accept far less ergonomic coding styles and the additional bug classes that only happen on async due to accidental blocking etc... despite the fact that when you consider a real-world deployed application, those "benefits" become indistinguishable from noise. However, due to the additional bug classes and worse ergonomics, there is now le…
Speaking of futures_unordered and similar patterns, I think a part of the "async promise" that has failed is the lack of concurrency for a single user request by default in most languages. That is, the 'easy' path is to write code such as the following (in vaguely C# pseudocode): var p = await GetUserPermission( username ); var c = await GetServerConfig(); var m = await GetMessageOfTheDay(); Assume each await call is…