Replace SipHash with identity hasher on all hot-path HashMaps keyed by ActorAddress. Since addresses are crypto-random, the first 8 bytes serve as an excellent hash directly. Microbenchmarks show 1.9-4.6x lookup speedup depending on map size. - Custom Hash impl for ActorAddress (8-byte write_u64 instead of 32) - AddrHasher/AddrBuildHasher identity hasher in delivery.rs - AddrMap<V>/AddrSet type aliases used in 7 HashMap sites - Stop-requests is_empty() short-circuit in tick_all inner loop - 3 new behavioral tests, component microbenchmarks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.2 KiB
Dispatch Model Comparison: Stakker vs Actix vs Swactor
Stakker — Closure-Based Dispatch (No HashMap, No Downcast)
Architecture: Single-threaded, synchronous actor runtime (like swactor). Messages are FnOnce closures, not typed structs.
Key Design:
call!macro turns method calls into closures:call!([actor], method(arg1, arg2))generates aFnOncethat directly calls the method on the actor- The closure captures the actor reference and method pointer — dispatch is a direct function call, not a downcast
- Closures stored in a flat heterogeneous FnOnce queue (byte Vec) — no per-message heap allocation
- Compiler can inline the closure, reducing message handling to "a single branch to optimised inlined code"
Why It's Fast:
- Zero
Box<dyn Any>allocation per message - Zero
TypeIddowncast per message - Zero HashMap lookup per message (actors addressed by direct
ActorOwn<A>references, not opaque addresses) - Queue is a flat contiguous memory region — excellent cache locality
Tradeoffs:
- Single-threaded only (no multi-worker routing)
- No location-transparent addresses (can't route to remote actors)
- Uses
unsafecode by default for the FnOnce queue
Actix — Vtable Dispatch (No HashMap, No Downcast)
Architecture: Async actor framework on tokio. Each actor is a pollable Future on an Arbiter thread.
Key Design:
Addr<A>wraps anAddressSender<A>— a direct channel reference, not an address in a map- Messages wrapped as
Box<dyn EnvelopeProxy<A>>— vtable dispatch, notBox<dyn Any>downcast - Custom Vyukov lock-free MPSC queue (single
AtomicPtr::swapfor push) - Default mailbox capacity: 16
Why It's Fast:
Addr<A>is a direct channel reference — zero HashMap lookup per sendEnvelopeProxyvtable call — one virtual dispatch, no TypeId comparison- Vyukov MPSC queue — lock-free push, minimal atomic operations
do_send()bypasses backpressure for internal messages
Tradeoffs:
- Requires async runtime (tokio dependency)
Addr<A>is typed — can't send different message types withoutRecipient<M>adaptationdo_send()silently drops messages to closed actors (no error feedback)
Swactor — Type-Erased Dispatch with HashMap Routing
Architecture: Synchronous tick-based runtime with multi-worker support. Messages are Box<dyn Any + Send>.
Message Send Path (per-message costs):
Box::new(msg)— heap allocation (~10-15ns)address_map.lookup(&addr)— RwLock read + HashMap get with 32-byte key- Push to VecDeque or crossbeam queue
Message Process Path (per-message costs):
slot.mailbox.pop_front()— VecDeque popmsg.downcast::<Incoming>()— TypeId comparison (~1ns)catch_unwind(|| actor.handle_any(ctx, msg))— unwind setup (~3-5ns)
Why It's Slower on Paper:
- HashMap lookup on every send AND every deliver (2 lookups per message)
- ActorAddress is 32 bytes — expensive to hash (SipHash: ~15ns for 32 bytes)
Box<dyn Any>heap allocation on every send
Why This Architecture Exists:
- Location-transparent 32-byte addresses enable multi-worker routing, transport layer, and distribution
- Type erasure enables heterogeneous mailboxes and type-agnostic forwarding
- HashMap enables O(1) address → worker lookup for any actor, from any thread
Optimization Applied: Identity Hashing
Problem: Every HashMap operation hashes 32 bytes of ActorAddress with SipHash.
Solution (two-layer):
- Custom
Hashimpl on ActorAddress — only hashes first 8 bytes viawrite_u64(SipHash on 8 bytes is ~3x faster than 32 bytes) - Identity hasher (
AddrHasher) on hot-path HashMaps — uses the 8-byte hash value directly as the bucket index, skipping SipHash entirely
Microbenchmark Results (same-process A/B comparison, reliable):
| Operation | SipHash (8-byte) | Identity | Speedup |
|---|---|---|---|
| Hash | 1.35 ns | 0.67 ns | 2.0x |
| Lookup (100 entries) | 29.8 ns | 15.5 ns | 1.9x |
| Lookup (1000 entries) | 38.0 ns | 8.2 ns | 4.6x |
| Insert 1000 | 48.2 µs | 21.1 µs | 2.3x |
Why It's Safe: ActorAddress bytes come from getrandom — cryptographically random, providing excellent uniform distribution without additional mixing.
Summary Table
| Aspect | Stakker | Actix | Swactor (optimized) |
|---|---|---|---|
| Message type | FnOnce closure |
Box<dyn EnvelopeProxy> |
Box<dyn Any + Send> |
| Dispatch | Direct call / inlined | Vtable call | TypeId downcast |
| Address lookup | None (direct ref) | None (direct channel) | Identity-hash HashMap |
| Per-msg alloc | None (flat queue) | Box (MPSC node) | Box (heap + VecDeque) |
| Multi-thread | No | Yes (tokio) | Yes (worker threads) |
| Location transparency | No | No | Yes (32-byte address) |
| Remote transport | No | No (without extras) | Yes (pluggable) |
Key Insight
Stakker and Actix are faster because they avoid the per-message HashMap lookup entirely — addresses are direct references, not opaque identifiers that need routing. Swactor pays for HashMap routing because its 32-byte addresses enable multi-worker distribution and remote transport. The identity hasher minimizes this cost without changing the architecture.