Benchmarks: the choose after the scheduler fix
A chain of two fibers no longer moves between cores when the inline budget runs out, and the skewed choose takes 181 ns at best, not 230. The ring rows move by a nanosecond or two, and the spawn row to 210 ns. The text says that the spawn row is slow only on many cores: on two or four, the current scheduler spawns faster than the pool did. 🤖 Generated with [ECA](https://eca.dev) Co-Authored-By: eca-agent <git@eca.dev>
This commit is contained in:
parent
6fef5a9c5f
commit
970754b3be
1 changed files with 8 additions and 8 deletions
|
|
@ -926,15 +926,15 @@ Go is the obvious point of comparison: goroutines and unbuffered channels are th
|
||||||
@read-table{
|
@read-table{
|
||||||
| Benchmark | Bjolang, @code{sync} | Bjolang, @code{chan-put} | Go | Go, with @code{ctx.Done()} |
|
| Benchmark | Bjolang, @code{sync} | Bjolang, @code{chan-put} | Go | Go, with @code{ctx.Done()} |
|
||||||
|----------------------------------------+----------------------+--------------------------+-----+----------------------------|
|
|----------------------------------------+----------------------+--------------------------+-----+----------------------------|
|
||||||
| Ring: 1000 fibers, a million messages | 71 | 75 | 117 | 241 |
|
| Ring: 1000 fibers, a million messages | 70 | 73 | 117 | 241 |
|
||||||
| The same ring inside a nested scope | 63 | — | — | 242 |
|
| The same ring inside a nested scope | 63 | — | — | 242 |
|
||||||
| Spawning a million fibers and waiting | 226 | — | 190 | — |
|
| Spawning a million fibers and waiting | 210 | — | 190 | — |
|
||||||
| An 8-way choose where one branch fires | 230 | 221 | 403 | 451 |
|
| An 8-way choose where one branch fires | 181 | 205 | 403 | 451 |
|
||||||
}
|
}
|
||||||
|
|
||||||
In the choose row only the sender can use @code{chan-put}, since the receiving side has to use channel-receive (and thus allocates). On the ring, syncing an event costs nothing measurable any more: a send or a receive synced alone does not build an event. For some reason, this ring inside a nested scope is faster in bjolang, and I haven't figured out why yet.
|
In the choose row only the sender can use @code{chan-put}, since the receiving side has to use channel-receive (and thus allocates). On the ring, syncing an event costs nothing measurable any more: a send or a receive synced alone does not build an event. For some reason, this ring inside a nested scope is faster in bjolang, and I haven't figured out why yet.
|
||||||
|
|
||||||
The spawn and choose rows are about twice what they were with the scheduler Bjolang used before, which handed every fiber to the .NET thread pool. With it (@code{BJO_SCHEDULER=pool} in the environment, kept as a way back) a spawn costs 105 ns and the choose 113. The scheduler that replaced it runs fibers from run queues of its own, in the shape of Go's, because the pool let work that had waited longest wait longer still under load. That is worth more in a loaded service, as the service benchmark below shows, than these two rows on an idle machine.
|
The spawn row is about twice what it was with the scheduler Bjolang used before, which handed every fiber to the .NET thread pool, and the choose row about one and a half times. With it (@code{BJO_SCHEDULER=pool} in the environment, kept as a way back) a spawn costs 105 ns and the choose 113. The spawn row is a single fiber spawning a million fibers that do nothing, and it is slow only on many cores: on two or four cores the current scheduler spawns faster than the pool, 61 and 72 ns against 87 and 112. The scheduler that replaced it runs fibers from run queues of its own, in the shape of Go's, because the pool let work that had waited longest wait longer still under load. That is worth more in a loaded service, as the service benchmark below shows, than these two rows on an idle machine.
|
||||||
|
|
||||||
These are small benchmarks on one machine, and they only measure the cost of the concurrency primitives, not of the work between rendezvous. Go is ahead in other ways: a Go channel operation never allocates, where a Bjolang @code{choose} allocates 56 bytes and a spawn 136 (a send or a receive synced alone allocates nothing). Any Go function can block without being written as a bjoroutine and the Go scheduler preempts a goroutine that computes for too long, where a fiber has to be moved off the pool with @code{spawn/thread}.
|
These are small benchmarks on one machine, and they only measure the cost of the concurrency primitives, not of the work between rendezvous. Go is ahead in other ways: a Go channel operation never allocates, where a Bjolang @code{choose} allocates 56 bytes and a spawn 136 (a send or a receive synced alone allocates nothing). Any Go function can block without being written as a bjoroutine and the Go scheduler preempts a goroutine that computes for too long, where a fiber has to be moved off the pool with @code{spawn/thread}.
|
||||||
|
|
||||||
|
|
@ -945,14 +945,14 @@ Hopac is the tougher comparison. It is a Concurrent ML library for F#, running o
|
||||||
@read-table{
|
@read-table{
|
||||||
| Benchmark | Bjolang, @code{sync} | Hopac | Hopac, racing a cancel @code{IVar} |
|
| Benchmark | Bjolang, @code{sync} | Hopac | Hopac, racing a cancel @code{IVar} |
|
||||||
|-----------------------------------------+----------------------+-------+------------------------------------|
|
|-----------------------------------------+----------------------+-------+------------------------------------|
|
||||||
| Ring: 1000 fibers, a million messages | 71 | 51 | 3487 |
|
| Ring: 1000 fibers, a million messages | 70 | 51 | 3487 |
|
||||||
| The same ring, one cancel IVar per job | — | — | 195 |
|
| The same ring, one cancel IVar per job | — | — | 195 |
|
||||||
| The same ring inside a nested scope | 63 | — | 3427 |
|
| The same ring inside a nested scope | 63 | — | 3427 |
|
||||||
| Spawning a million fibers and waiting | 226 | 80 | — |
|
| Spawning a million fibers and waiting | 210 | 80 | — |
|
||||||
| An 8-way choose where one branch fires | 230 | 127 | 136 |
|
| An 8-way choose where one branch fires | 181 | 127 | 136 |
|
||||||
}
|
}
|
||||||
|
|
||||||
Without cancellation Hopac is ahead: its ring takes 30% less time than Bjolang's, its choose a little over half, and its spawns about a third. Bjolang's numbers include watching the scope's token at every operation, which Hopac's plain rows do not do. Once cancellation is added, the ring turns. A thousand jobs racing one shared @code{IVar} all take that IVar's lock at every operation, and the ring slows to three and a half microseconds a message. Giving each job an IVar of its own brings it back to 195 ns, at the price of keeping track of a thousand IVars and filling all of them to stop the ring. The choose does not turn: with a cancel IVar Hopac's takes 136 ns, against Bjolang's 230. Hopac allocates more: 208 bytes a message on the ring and 424 a choose, against Bjolang's 0 and 56. Hopac can run jobs in parallel and wait for all of them, but a job cannot be stopped from the outside, so stopping jobs when one of them fails, or when their owner is done with them, is the same hand-written race against an IVar as in the table.
|
Without cancellation Hopac is ahead: its ring and its choose take about 30% less time than Bjolang's, and its spawns about a third. Bjolang's numbers include watching the scope's token at every operation, which Hopac's plain rows do not do. Once cancellation is added, the ring turns. A thousand jobs racing one shared @code{IVar} all take that IVar's lock at every operation, and the ring slows to three and a half microseconds a message. Giving each job an IVar of its own brings it back to 195 ns, at the price of keeping track of a thousand IVars and filling all of them to stop the ring. The choose does not turn: with a cancel IVar Hopac's takes 136 ns, against Bjolang's 181. Hopac allocates more: 208 bytes a message on the ring and 424 a choose, against Bjolang's 0 and 56. Hopac can run jobs in parallel and wait for all of them, but a job cannot be stopped from the outside, so stopping jobs when one of them fails, or when their owner is done with them, is the same hand-written race against an IVar as in the table.
|
||||||
|
|
||||||
@subsubsection{A service under load}
|
@subsubsection{A service under load}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue