8 ms·
> fibers can context switch billions of times per second on a modern processor. On a 3GHz (3 billion hertz) processor, you expect to be able to context switch
by coder543 8y ago
> fibers can context switch billions of times per second on a modern processor.
On a 3GHz (3 billion hertz) processor, you expect to be able to context switch billions of times per second?
I would probably accept millions without question, even though that might be pushing it for a GIL-ed runtime like Ruby has. But, unless your definition of "context switch" counts every blocked fiber that's passed over for a context switch as being implicitly context switched to and away from in the act of ignoring it, I find this hard to believe.
It takes more than one clock cycle to determine the next task that we're going to resume execution for and then actually resume it.
- devxpy 8y agoWould love to see some numbers supporting this.. He might have mis-phrased it though. Maybe he meant to say that it can handle billions of concurrent tasks maybe not that they can switch rapidly in just a second xD
- fake-name 8y agoI can only assume they're talking about the entire cpu (as in, all 4+ cores in a single die) rather then a single logical core in the hardware. It's sloppy wording either way.
- ioquatix 8y agoHi, I'm the author, and I agree, so since everyone is so interested I'll check my original benchmark and fix the article. Update: I wrote an addendum https://www.codeotaku.com/journal/2018-11/fibers-are-the-right-solution/context-switching https://www.codeotaku.com/journal/2018-11/fibers-are-the-rig...
- erdewit 8y agoIt must be off by 3 of 4 orders of magnitude. Python with asyncio does about 120_000 switches per second with a trivial fibre/coroutine here: import time import asyncio REPS = 1_000_000 async def coro(): for i in range(REPS): await asyncio.sleep(0) t0 = time.time() asyncio.run(coro()) dt = time.time() - t0 print(REPS / dt, 'reps/s')
- blattimwind 8y agoasyncio eats about one order of magnitude. A more minimalistic async-approach gives something on the order of ~900000 resumptions per second. (Single-thread hardware performance differences would not account for a factor 10)
- blattimwind 8y agoSlightly modified "benchmark" from above: asyncio: import time import asyncio REPS = 1_000_000 async def coro(): for i in range(REPS): await asyncio.sleep(0) t0 = time.perf_counter() asyncio.run(coro()) dt = time.perf_counter() - t0 print(REPS / dt, 'reps/s') ~180k asynker (https://github.com/enkore/asynker https://github.com/enkore/asynker): import time import asynker REPS = 1_000_000 async def coro(): for i in range(REPS): await asynker.suspend() t0 = time.perf_counter() sched = asynker.Scheduler() sched.run_until_complete(coro()) dt = time.perf_counter() - t0 print(REPS / dt, 'reps/s') ~900k The excellent curio (https://github.com/dabeaz/curio https://github.com/dabeaz/curio): import time import curio REPS = 1_000_000 async def coro(): for i in range(REPS): await curio.sleep(0) t0 = time.perf_counter() curio.run(coro()) dt = time.perf_counter() - t0 print(REPS / dt, 'reps/s') ~180k Not that this number is hugely important.
- wenc 8y agoWould uvloop change these benchmarks? https://github.com/MagicStack/uvloop https://github.com/MagicStack/uvloop
- blattimwind 8y agoIt actually does, but again, this is a "how fast can you do nothing" microbenchmark. It's a bit of trivia for almost all intents and purposes. (The number's 500k)
- ovi256 8y agoGolang seems to do around 350k. There's a chance I'm missing some tricks, but the code is so short it's not probable I'm missing that much. See: https://repl.it/repls/GrimyChiefGame https://repl.it/repls/GrimyChiefGame package main import ( "fmt" "time" ) func coro(reps int) { for i := 0; i < reps; i++ { go time.Sleep(0 * time.Nanosecond) } } func main() { REPS := 5000000 start := time.Now() coro(REPS) dt := time.Since(start) fmt.Printf("The call took %v to run.\n", dt) fmt.Printf("REPS / duration %v\n", (REPS*1e9)/int(dt)) }
- tomp 8y ago> It takes more than one clock cycle to determine the next task that we're going to resume execution for and then actually resume it. Not really, if you just implement a round-robin scheduler and if none of the fibres are actually blocked (i.e. if they just yield). Artificial benchmark? Sure. But there's no way you could ever approximate this with OS threads, even if the user-space code/threads themselves do no useful work.
- wahern 8y agoThe difference between stackful coroutines (i.e. fibers in this case) and Python-style coroutines is that Python's coroutines need to rebuild the stack on every resume, whereas resuming a fiber is basically a goto. The cost of yielding and resuming a coroutine in Python is O(N) for the depth of the call chain, but O(1) for fibers. So as you say, a simple scheduler that just walks a linked-list could resume a stackful coroutine in a few instructions, plus the cost of restoring hardware registers (which is the same cost as a non-inlined function call), and the latter is easily pipelined so would take fewer cycles than instructions.
- tomp 8y agoIndeed, a highly-optimized scheduler is basically the same as looping through an array and calling virtual methods of each object. Extremely cheap.