Live data from Hacker News

Comparison of Rust async and Linux thread context switch time and memory use

github.com

111–120 of 201 posts

Re: Comparison of Rust async and Linux thread context switch time and memory use

#111
post #51

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.

I work in ultra-low latency space and agree with GP. This comparison makes no sense as OS-level context switch is completely different from a task-switch within the same native thread. The Rust ones from that benchmark are essentially fibers, not threads. You will see similar performance for switching fibers if well implemented in Java, C++ or other natively compiled language. This has nothing to do with Rust.

Re: Comparison of Rust async and Linux thread context switch time and memory use

#112
post #95

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

You have to do so much more to be able to reliably measure events on the scale of nanos. You need to lock C-states, disable P-state driver, isolate CPUs, get rid of RCUs, affinitize your tasks, enable low-tick mode, skew hr ticks, make sure you use TSC clocksource, set the cpu governor, get rid of vmstat, set correct idle driver, disable audits, and watchdogs and much, much more.

Re: Comparison of Rust async and Linux thread context switch time and memory use

#113
post #32
post #4

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

You're only the second commenter on this thread to notice this.

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

#114
post #79
post #66

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

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

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

#115
post #101
post #87

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

This is perfectly valid for NodeJS, Python (async and/or threads, hello GIL[0]), and a host of other languages/runtimes.

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

#116

The problem with threads is you need to correctly size your thread pool. That's hugely difficult if you have unknown lengths of blocking IO.

You can use a dynamically sized thread pool. E.g. remove a thread with a certain probability once it's idle for more than X seconds.

Re: Comparison of Rust async and Linux thread context switch time and memory use

#117
post #83

Earlier 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?

> though obviously only works when you can « afford » a multithreaded scheduler.

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

#118
post #53

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

I understand the desire to stave off dependencies but managing an async runtime should only be a simple function call or two. How do you end up locking up the runtime with something like that?

Re: Comparison of Rust async and Linux thread context switch time and memory use

#119
post #75

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

That .NET syntax using Task.WhenAll seems quite bad, which might be part of the reason why not many people bother (disclaimer: I don't do C# or ASP.NET). In Rust it would be:

    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
post #75

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

I guess I am that no one. I come at all this from writing queues from scratch and using threads or processes for concurrency. I also had a lot of fun writing my own networking hot loops with select/poll/spill/kqueue when my work needed it, so I guess I am extra sensitive to making concurrent things actually concurrent. But I would not dream of making three independent requests like that sequentially. There are other patterns you can use besides waiting for all tasks to finish, especially if you can do some processing after the first are done, but all in all why wouldn’t you make them concurrent aside from liking seeing await/async all over the place?
Post reply on HN