diff --git a/_02_.Writing-programs.sz b/_02_.Writing-programs.sz index d6460fb..22a5170 100644 --- a/_02_.Writing-programs.sz +++ b/_02_.Writing-programs.sz @@ -926,15 +926,15 @@ Go is the obvious point of comparison: goroutines and unbuffered channels are th @read-table{ | 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 | -| Spawning a million fibers and waiting | 226 | — | 190 | — | -| An 8-way choose where one branch fires | 230 | 221 | 403 | 451 | +| Spawning a million fibers and waiting | 210 | — | 190 | — | +| 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. -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}. @@ -945,14 +945,14 @@ Hopac is the tougher comparison. It is a Concurrent ML library for F#, running o @read-table{ | 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 inside a nested scope | 63 | — | 3427 | -| Spawning a million fibers and waiting | 226 | 80 | — | -| An 8-way choose where one branch fires | 230 | 127 | 136 | +| Spawning a million fibers and waiting | 210 | 80 | — | +| 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}