Benchmarks
These numbers measure the bus alone. Handlers only count what they get, or in v2’s own suite do nothing with it, so what’s left is the cost of queueing each message, waking the handlers’ goroutines, and waiting for room when they fall behind. Queues hold 1024 messages. Your handlers will dominate a real program; the point is that the bus adds as little as possible.
v2 against v1
Section titled “v2 against v1”Both versions ran on the same harness, with the same handlers and queue sizes. A “16 publishers” scenario publishes from 16 goroutines at once. There, and in the suite’s Parallel rows, the time is wall time per call across all of them, so it measures throughput.
Show as a table
| Scenario | v1 | v2 |
|---|---|---|
| 1 subscriber | 190.9 ns2 allocs | 48.6 ns0 allocs |
| 10 subscribers | 895.0 ns2 allocs | 449.3 ns0 allocs |
| 100 subscribers | 13.02 µs2 allocs | 9.81 µs0 allocs |
| 100 topics, 1 subscriber each | 146.8 ns2 allocs | 112.2 ns0 allocs |
Apple M4 Max, Go 1.27.0, median of 5 runs.
Show as a table
| Scenario | v1 | v2 |
|---|---|---|
| 1 subscriber | 347.5 ns2 allocs | 109.4 ns0 allocs |
| 100 topics, 1 subscriber each | 159.2 ns2 allocs | 27.7 ns0 allocs |
Apple M4 Max, Go 1.27.0, median of 5 runs.
Show as a table
| Scenario | v1 | v2 |
|---|---|---|
| Subscribe, then unsubscribe | 2.45 µs5 allocs | 1.12 µs6 allocs |
Apple M4 Max, Go 1.27.0, median of 5 runs.
v1 boxed every argument and called handlers through reflection, at two allocations per Publish; v2 copies the message into each queue and calls the handler directly, allocating nothing. v1 held one lock for the whole bus while it published; v2 spreads topics over 64 locks by their hash, which is where most of the gain with many publishers comes from.
With many subscribers, most of the time goes to the Go scheduler: each Publish wakes every subscriber’s goroutine that was waiting for a message. Both versions pay that.
v2’s own suite
Section titled “v2’s own suite”| Benchmark | Time | Memory | Allocations |
|---|---|---|---|
Publish/1One subscriber | 42.8 ns | 0 B | 0 |
Publish/10Ten subscribers to one topic | 423.7 ns | 0 B | 0 |
Publish/100A hundred subscribers to one topic | 8.67 µs | 0 B | 0 |
PublishTopicsA hundred topics in turn, one subscriber each | 103.9 ns | 0 B | 0 |
PublishUnbufferedNew(0): each message is handed straight to the handler | 155.5 ns | 0 B | 0 |
PublishParallelPublish/1 from 16 goroutines at once | 102.0 ns | 0 B | 0 |
PublishParallelTopicsPublishTopics from 16 goroutines at once | 27.6 ns | 0 B | 0 |
SubscribeSubscribe, then unsubscribe | 1.09 µs | 9736 B | 6 |
Run them yourself
Section titled “Run them yourself”From a clone of the repository:
go test -run '^$' -bench . -benchmemTo compare two versions, run each a few times and compare with benchstat:
go test -run '^$' -bench . -count 10 > new.txtbenchstat old.txt new.txtThe allocation counts are also enforced by tests: TestAllocations fails if Publish allocates while there is
room, or while it waits with a context that can’t be cancelled.