-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture Performance
This page shows the latest dispatch-throughput and cross-library comparison numbers for DxMessaging. The tables are auto-generated by CI: every pull request and push re-runs the benchmark suite with the .NET Standard 2.1 API profile and a Release code-optimization build, then renders the results into the AUTOGENERATED region below. Throughput is measured in a built Standalone player under IL2CPP in the Release configuration (no development build, Release C++ code generation) -- the ahead-of-time backend and build shape shipped games actually run. GC allocation counts come from an in-editor PlayMode (Mono) leg, because a Release player strips the profiler the allocation recorder needs; the counts are build-config-independent, so they represent the shipped player.
These numbers are for orientation, not a leaderboard. Real-world performance depends on what your handlers actually do; the benchmarks measure raw dispatch cost with minimal handler work. For the full methodology, CI mechanics, baseline capture, the regression smoke gate, and how to add or bump a comparison library, see the Perf Benchmark Methodology runbook.
See also: .-Design-And-Architecture#performance-optimizations for design details.
- Scopes. Each dispatch table is labeled by execution scope and backend. Standalone (IL2CPP) -- a Release player on the ahead-of-time backend shipped games run -- is the throughput headline. CI also publishes an in-editor PlayMode (Mono) leg, because a Release player strips the profiler and so cannot measure allocations; the Mono leg supplies the real GC-allocation numbers (see Allocations below). The renderer also understands EditMode rows, so additional scopes render automatically if a future workflow publishes them; backends differ by design, so read each scope against its own backend.
- Throughput. Reported as emits per second. Higher is better. Registration scenarios report wall-clock time instead, where lower is better. The published throughput numbers come from the Standalone (IL2CPP) leg.
-
Allocations. Reported as the COUNT of managed GC allocations (and a companion
byte total) observed over one measurement batch (lower is better;
0is best for hot-path dispatch). Both come from Unity'sGC.Allocprofiler recorder, which is only available where the profiler is present. A Release IL2CPP player strips that recorder, so the Standalone tables would have nothing to put in their memory columns -- rather than publish a column ofn/a, those tables omit the memory columns entirely and show only throughput. The allocation and byte numbers come from the in-editor PlayMode (Mono) leg, where the recorder is functional; counts are build-config-independent (the same code paths box and allocate under Mono and IL2CPP), so the Mono numbers represent the shipped IL2CPP player.n/aappears only as an individual cell -- when a metric is measured for a scope or library in general but missing for that one row -- never as a whole vacuous column or matrix; it is never rendered as a misleading0, and a measured0is a real zero-allocation result. (This replaces an earlier byte counter built onGC.GetAllocatedBytesForCurrentThread(), which returns0for every allocation under Unity's Boehm GC and so reported a vacuous0for every technology -- see the runbook.) -
Comparison matrix
N/A. The cross-library matrix has a column per scenario and a row per library. A cell showsN/Awhen that library does not idiomatically support that capability -- it is a capability gap, not a failure, and the value is never faked. -
Comparison matrix winners. In the throughput matrix the fastest
technology per scenario column is rendered in bold (ties are all bolded;
N/Anever wins). The GC-allocations and GC-allocated-bytes matrices are not bolded: an allocation count or byte total is a property to read, not a race.
The block below is regenerated by the Performance Numbers workflow
(.github/workflows/perf-numbers.yml) via scripts/unity/render-perf-doc.js. It
contains one dispatch-throughput table per execution scope present in the run
(Standalone IL2CPP for throughput, plus an in-editor PlayMode Mono leg that
supplies the real GC-allocation counts, and byte totals once a run measures them)
plus the cross-library
comparison matrices -- one for throughput (sourced from the Standalone leg), and
one each for GC allocations and GC allocated bytes per batch, sourced from
whichever leg could measure them and omitted when none could. A scope that cannot
measure a metric (the profiler-stripped Standalone leg) omits that column rather
than filling it with n/a. The block also carries a privacy-safe provenance
line describing the runner hardware (CPU, cores, clock, RAM, GPU, OS), never a
hostname or runner name. Licensed benchmarks do not run on pull-request code.
After a pull request merges, the workflow commits the refreshed tables -- and
the sibling baseline
perf-baseline.csv that the regression gate compares against -- directly to the
default branch when the auto-commit App is provisioned and the branch has not
advanced past the measured commit. Do not edit it by hand. See the
perf-numbers auto-commit runbook for
the repo-settings prerequisite that lets CI push to the default branch.
Latest CI benchmark run: Unity 6000.3.16f1, commit ac1af7defbe60e2e8d7a6c377c3434881e864faa.
Runner: 13th Gen Intel(R) Core(TM) i9-13900KF, 24C/32T @ 3000MHz; 64GB DDR5@4200; NVIDIA GeForce RTX 3060; Microsoft Windows 11 Pro N (10.0.26200)
Platform: Standalone IL2CPP x64 Release (WindowsPlayer; Unity 6000.3.16f1).
| Scenario | Throughput / Wall clock |
|---|---|
| Empty Bus Dispatch | 47.21 M emits/sec |
| Untargeted Flood (One Handler) | 31.46 M emits/sec |
| Untargeted Flood (Two Handlers, One Priority) | 32.32 M emits/sec |
| Untargeted Flood (Three Handlers, One Priority) | 25.39 M emits/sec |
| Untargeted Flood (Four Handlers, One Priority) | 29.05 M emits/sec |
| Untargeted Flood (Four Handlers, Four Priorities) | 29.22 M emits/sec |
| Untargeted Flood (Sixteen Handlers, One Priority) | 17.13 M emits/sec |
| Untargeted Flood (One Inactive Handler) | 36.03 M emits/sec |
| Untargeted First Dispatch (Cold, Distinct Types) | 0.243 ms |
| Targeted Flood (No Matching Target) | 14.37 M emits/sec |
| Targeted Flood (One Listener) | 11.40 M emits/sec |
| Targeted Flood (Sixteen Listeners) | 7.71 M emits/sec |
| Targeted First Dispatch (Cold, Distinct Types) | 0.189 ms |
| Broadcast Flood (One Handler) | 21.19 M emits/sec |
| Broadcast First Dispatch (Cold, Distinct Types) | 0.188 ms |
| Interceptor Heavy (Four Interceptors) | 3.98 M emits/sec |
| Post-Processing Heavy (Four Post-Processors) | 13.57 M emits/sec |
| Message Bus Construction (1000) | 16.269 ms |
| Registration Token Construction (1000, Prebuilt Handler + Bus) | 0.050 ms |
| Registration Flood (1000 Types, Cold Bus) | 541.150 ms |
| Registration Flood (1000 Types, Warm JIT) | 3.397 ms |
| Untargeted Registration (Marginal, 1000 Same-Type) | 0.403 ms |
| Targeted Registration (Marginal, 1000 Same-Type) | 0.458 ms |
| Broadcast Registration (Marginal, 1000 Same-Type) | 0.453 ms |
| Deregistration Flood (1000 Types, Cold) | 1.850 ms |
| Deregistration Flood (1000 Types, Warm JIT) | 1.144 ms |
Platform: Editor PlayMode Mono x64 Release (WindowsEditor; Unity 6000.3.16f1).
| Scenario | Throughput / Wall clock | GC allocs | GC bytes |
|---|---|---|---|
| Empty Bus Dispatch | 24.90 M emits/sec | 0 | 0 |
| Untargeted Flood (One Handler) | 20.87 M emits/sec | 0 | 0 |
| Untargeted Flood (Two Handlers, One Priority) | 18.11 M emits/sec | 0 | 0 |
| Untargeted Flood (Three Handlers, One Priority) | 16.32 M emits/sec | 0 | 0 |
| Untargeted Flood (Four Handlers, One Priority) | 15.90 M emits/sec | 0 | 0 |
| Untargeted Flood (Four Handlers, Four Priorities) | 14.43 M emits/sec | 0 | 0 |
| Untargeted Flood (Sixteen Handlers, One Priority) | 8.21 M emits/sec | 0 | 0 |
| Untargeted Flood (One Inactive Handler) | 19.74 M emits/sec | 0 | 0 |
| Untargeted First Dispatch (Cold, Distinct Types) | 3.097 ms | 41 | 11,318 |
| Targeted Flood (No Matching Target) | 21.22 M emits/sec | 0 | 0 |
| Targeted Flood (One Listener) | 13.76 M emits/sec | 0 | 0 |
| Targeted Flood (Sixteen Listeners) | 7.10 M emits/sec | 0 | 0 |
| Targeted First Dispatch (Cold, Distinct Types) | 7.497 ms | 42 | 19,876 |
| Broadcast Flood (One Handler) | 12.99 M emits/sec | 0 | 0 |
| Broadcast First Dispatch (Cold, Distinct Types) | 5.838 ms | 41 | 19,534 |
| Interceptor Heavy (Four Interceptors) | 2.36 M emits/sec | 0 | 0 |
| Post-Processing Heavy (Four Post-Processors) | 9.92 M emits/sec | 0 | 0 |
| Message Bus Construction (1000) | 34.657 ms | 40,009 | 2,480,640 |
| Registration Token Construction (1000, Prebuilt Handler + Bus) | 0.084 ms | 1,000 | 120,000 |
| Registration Flood (1000 Types, Cold Bus) | 3498.660 ms | 52,145 | 3,813,528 |
| Registration Flood (1000 Types, Warm JIT) | 11.393 ms | 30,059 | 2,489,888 |
| Untargeted Registration (Marginal, 1000 Same-Type) | 0.498 ms | 2,030 | 640,484 |
| Targeted Registration (Marginal, 1000 Same-Type) | 0.566 ms | 2,030 | 672,484 |
| Broadcast Registration (Marginal, 1000 Same-Type) | 0.583 ms | 2,030 | 672,484 |
| Deregistration Flood (1000 Types, Cold) | 92.628 ms | 42 | 116,856 |
| Deregistration Flood (1000 Types, Warm JIT) | 1.795 ms | 44 | 116,952 |
| Technology | Global -> 1 subscriber | Global -> 16 subscribers | Keyed/targeted -> 1 of many | Priority-ordered dispatch | Filtered/intercepted dispatch | Post-processing dispatch | Subscribe/unsubscribe churn | Struct message (no boxing) |
|---|---|---|---|---|---|---|---|---|
| DxMessaging | 27.58 M emits/sec | 13.27 M emits/sec | 9.07 M emits/sec | 23.87 M emits/sec | 7.03 M emits/sec | 12.64 M emits/sec | 0.88 M emits/sec | 30.26 M emits/sec |
| MessagePipe | 59.21 M emits/sec | 13.42 M emits/sec | 9.37 M emits/sec | N/A | 51.41 M emits/sec | N/A | 1.83 M emits/sec | 67.26 M emits/sec |
| UniRx MessageBroker | 4.02 M emits/sec | 2.25 M emits/sec | N/A | N/A | N/A | N/A | 0.76 M emits/sec | 4.34 M emits/sec |
| Zenject SignalBus | 2.00 M emits/sec | 1.07 M emits/sec | N/A | N/A | N/A | N/A | 1.52 M emits/sec | 2.03 M emits/sec |
| Unity Atoms | 70.87 M emits/sec | 32.54 M emits/sec | 86.55 M emits/sec | N/A | N/A | N/A | 9.90 M emits/sec | N/A |
| ScriptableObject channel | 104.97 M emits/sec | 18.94 M emits/sec | 129.24 M emits/sec | N/A | N/A | N/A | 30.51 M emits/sec | 143.52 M emits/sec |
| UnityEvent | 85.05 M emits/sec | 9.22 M emits/sec | 93.75 M emits/sec | N/A | N/A | N/A | 3.50 M emits/sec | 79.27 M emits/sec |
| C# event | 286.19 M emits/sec | 51.26 M emits/sec | 58.14 M emits/sec | N/A | N/A | N/A | 10.09 M emits/sec | 256.72 M emits/sec |
| Unity SendMessage | 7.32 M emits/sec | 0.91 M emits/sec | 6.77 M emits/sec | N/A | N/A | N/A | N/A | N/A |
| Technology | Global -> 1 subscriber | Global -> 16 subscribers | Keyed/targeted -> 1 of many | Priority-ordered dispatch | Filtered/intercepted dispatch | Post-processing dispatch | Subscribe/unsubscribe churn | Struct message (no boxing) |
|---|---|---|---|---|---|---|---|---|
| DxMessaging | 0 | 0 | 0 | 0 | 0 | 0 | 80,000 | 0 |
| MessagePipe | 0 | 0 | 0 | N/A | 0 | N/A | 20,000 | 0 |
| UniRx MessageBroker | 0 | 0 | N/A | N/A | N/A | N/A | 150,000 | 0 |
| Zenject SignalBus | 20,000 | 20,000 | N/A | N/A | N/A | N/A | 70,000 | 20,000 |
| Unity Atoms | 0 | 0 | 0 | N/A | N/A | N/A | 0 | N/A |
| ScriptableObject channel | 0 | 0 | 0 | N/A | N/A | N/A | 0 | 0 |
| UnityEvent | 0 | 0 | 0 | N/A | N/A | N/A | 50,000 | 0 |
| C# event | 0 | 0 | 0 | N/A | N/A | N/A | 0 | 0 |
| Unity SendMessage | 10,008 | 10,000 | 10,000 | N/A | N/A | N/A | N/A | N/A |
| Technology | Global -> 1 subscriber | Global -> 16 subscribers | Keyed/targeted -> 1 of many | Priority-ordered dispatch | Filtered/intercepted dispatch | Post-processing dispatch | Subscribe/unsubscribe churn | Struct message (no boxing) |
|---|---|---|---|---|---|---|---|---|
| DxMessaging | 0 | 0 | 0 | 0 | 0 | 0 | 7,560,000 | 0 |
| MessagePipe | 0 | 0 | 0 | N/A | 0 | N/A | 560,000 | 0 |
| UniRx MessageBroker | 0 | 0 | N/A | N/A | N/A | N/A | 6,650,000 | 0 |
| Zenject SignalBus | 600,000 | 600,000 | N/A | N/A | N/A | N/A | 3,200,000 | 600,000 |
| Unity Atoms | 0 | 0 | 0 | N/A | N/A | N/A | 0 | N/A |
| ScriptableObject channel | 0 | 0 | 0 | N/A | N/A | N/A | 0 | 0 |
| UnityEvent | 0 | 0 | 0 | N/A | N/A | N/A | 2,960,000 | 0 |
| C# event | 0 | 0 | 0 | N/A | N/A | N/A | 0 | 0 |
| Unity SendMessage | 200,752 | 200,000 | 200,000 | N/A | N/A | N/A | N/A | N/A |
The cross-library comparison matrices above measure DxMessaging against other common Unity messaging and eventing approaches on the same apples-to-apples scenarios:
- External libraries: MessagePipe, UniRx MessageBroker, Zenject SignalBus, and Unity Atoms.
-
Zero-dependency baselines: plain C# event, UnityEvent, a ScriptableObject
event channel, and Unity
SendMessage.
Each library implements only the scenarios it idiomatically supports;
unsupported cells render N/A. The comparison suite source lives in
Tests/Runtime/Comparisons/.
For a feature-by-feature discussion of when each approach wins, see the
.-Comparisons.
Dispatch state is stored per message type and, for targeted and broadcast
paths, per InstanceId. Long-running sessions accumulate slots for every
type or entity ever touched unless something reclaims them. The memory
reclamation system caps that growth without changing dispatch semantics or
allocating during emit.
Reclamation runs on two paths:
- An idle sweep that runs from emit-time clock samples and the Unity
PlayerLoop, gated by
DxMessagingRuntimeSettings.EvictionEnabledandEvictionTickIntervalSeconds. Empty slots become eligible only after remaining empty for at leastIdleEvictionSecondsof wall time. - An explicit
IMessageBus.Trim(force)andMessageHandler.TrimAll(force)pair that runs synchronously at scene boundaries, in tests, or in maintenance windows. The master switchEnableTrimApicontrols whether the explicit calls perform work; idle sweeps remain controlled byEvictionEnabledindependently.
Active registrations are never reclaimed. Only empty slots and shared pool entries are touched. Sweep work runs outside the hot handler loop, so emit throughput is unaffected; the per-emit overhead is one branch that samples the wall clock.
For tuning recommendations, the public Trim and diagnostic-counter API
surface, and worked examples (scene transitions, leak diagnosis, mobile
caps, shipped-title configurations), see the
..-Guides-Memory-Reclamation. For the
parameter reference, see the
..-Reference-Runtime-Settings.
- Getting-Started-Overview
- Getting-Started-Getting-Started
- Getting-Started-Install
- Getting-Started-Quick-Start
- Getting-Started-Visual-Guide
- Concepts-Message-Types
- Concepts-Listening-Patterns
- Concepts-Targeting-And-Context
- Concepts-Interceptors-And-Ordering
- Guides-Patterns
- Guides-Unity-Integration
- Guides-Testing
- Guides-Diagnostics
- Guides-Advanced
- Guides-Migration-Guide
- Advanced-Emit-Shorthands
- Advanced-Message-Bus-Providers
- Advanced-Runtime-Configuration
- Advanced-String-Messages
- Reference-Reference
- Reference-Quick-Reference
- Reference-Helpers
- Reference-Faq
- Reference-Glossary
- Reference-Troubleshooting
- Reference-Compatibility
Links