# 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 a `FnOnce` that 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` allocation per message - Zero `TypeId` downcast per message - Zero HashMap lookup per message (actors addressed by direct `ActorOwn` 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 `unsafe` code 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` wraps an `AddressSender` — a direct channel reference, not an address in a map - Messages wrapped as `Box>` — vtable dispatch, not `Box` downcast - Custom Vyukov lock-free MPSC queue (single `AtomicPtr::swap` for push) - Default mailbox capacity: 16 **Why It's Fast**: - `Addr` is a direct channel reference — zero HashMap lookup per send - `EnvelopeProxy` vtable 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` is typed — can't send different message types without `Recipient` adaptation - `do_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`. **Message Send Path** (per-message costs): 1. `Box::new(msg)` — heap allocation (~10-15ns) 2. `address_map.lookup(&addr)` — RwLock read + HashMap get with 32-byte key 3. Push to VecDeque or crossbeam queue **Message Process Path** (per-message costs): 1. `slot.mailbox.pop_front()` — VecDeque pop 2. `msg.downcast::()` — TypeId comparison (~1ns) 3. `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` 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): 1. Custom `Hash` impl on ActorAddress — only hashes first 8 bytes via `write_u64` (SipHash on 8 bytes is ~3x faster than 32 bytes) 2. 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` | `Box` | | 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.