promotional bannermobile promotional banner

Fast Events

Dispatch-path optimizations for event bus

Fast-Events makes the Forge / NeoForge event bus fast. It is a drop-in jar: no configuration, no dependencies, no Mixin, no changes to any other mod. Every event post that it takes over becomes a direct, fully inlined call chain instead of a megamorphic loop — so the cost of posting an event drops to roughly what it costs to call your listeners by hand.

It accelerates every mod's event posts, not only its own: the jar rewrites post call sites in all loaded classes, so mods that post a lot (ticking, block/entity updates, capability events, screen events) get faster without being modified.

Which file do I need?

Minecraft Loader File
1.20.1 Forge 47.x fastevents-1.20-*.jar
1.21.1 NeoForge 21.1.x fastevents-1.21-*.jar

One jar per game version. Nothing else is required.

The problem it solves

Both bus implementations dispatch like this:

for (EventListener listener : listeners) {
    listener.invoke(event);        // one call site, every listener class in the game
}

That single call site sees every listener implementation that any mod ever registered, so it is megamorphic: the JIT cannot inline any of them, and every single listener call pays a virtual dispatch plus the wrapper layers around it. On a big modpack most of the per-tick event cost is exactly this overhead, not your actual listener code.

What Fast-Events does

  • Per call site, generated dispatcher. The first time a post site fires for an event class, a dispatcher is generated for that class's listener array: one field and one call instruction per listener. Each call site therefore sees exactly one receiver type and inlines all the way down into the listener body.
  • No event object when nobody listens. If an event class has no listeners, the new Event() at the call site is short-circuited and never constructed.
  • Same semantics. Priority order, phase markers, cancellation, generic type filters and exception handling behave exactly as in vanilla; the generated code is a straight-line expansion of what the bus would have done.
  • Live cache. Dispatchers are rebuilt when a listener array changes, so listeners registered late (after a class was already dispatching) still take effect.

Numbers

Nanoseconds per post, single thread, 8 listeners on one event class, Java 21. Lower is better. Measured with the benchmark harness in the repository.

Minecraft 1.20.1 / Forge 47.4 vanilla Fast-Events  
@SubscribeEvent listeners 49.6 ns 9.3 ns 5.3×
addListener(this::handle), 8 different refs 46.4 ns 8.2 ns 5.7×
addListener(consumer) 15.7 ns 5.8 ns 2.7×
generic listeners (AttachCapabilitiesEvent<T> style) 52.2 ns 12.4 ns 4.2×
8 listeners spread over 5 priorities — 5.7 ns  
128 listeners on one class 201 ns 88 ns 2.3×
512 listeners on one class 1090 ns 556 ns 2×
Minecraft 1.21.1 / NeoForge 21.1 vanilla Fast-Events  
@SubscribeEvent listeners 31.2 ns 8.8 ns 3.5×
addListener(consumer) 19.0 ns 8.8 ns 2.2×
new Event(), escaped (no listeners at all) — ~2.4 ns  

Wider listener arrays are the common case in real packs, and they stay ahead of vanilla too. Absolute savings scale with how often a pack posts events: a listener call that used to cost 50 ns and now costs 9 ns matters when it runs thousands of times per tick across a pack.

Install

  1. Install Minecraft 1.20.1 with Forge 47.x, or Minecraft 1.21.1 with NeoForge 21.1.x.
  2. Put the matching jar into your mods/ folder. (It also works on the launcher classpath.)
  3. That's it. No config file, no dependencies, nothing to set up.

Client and server are both supported, and both sides benefit. For a server, install it on the server; clients that join do not need it (but get a small gain if they have it too).

Optional settings

Only useful when debugging; the default configuration is what you want for playing.

System property Effect
-Dfastevents.guard=false Disable the "skip new Event() when nobody listens" shortcut.
-Dfastevents.audit=true Verify the cached listener array on every post and log late registrations.
-Dfastevents.diag=true 1.20.1 only: log listeners registered through a path that is not rewritten.

Compatibility

  • Forge 47.x (1.20.1, event bus 6.0.x / 6.2.x) and NeoForge 21.1.x (1.21.1, event bus 8.0.x). Other 1.20.x / 1.21.x builds are not supported.
  • No Mixin, no access transformers, no coremod entry point that other performance mods fight over. It is a launch plugin that only replaces post/addListener call sites.
  • Does not modify Forge, NeoForge or the event bus classes themselves, and does not touch another mod's listeners or registration data.
  • Safe to remove at any time — nothing is written to your world or configs.
  • Not accelerated, and left on the vanilla path (so there is no behavioural difference): on 1.21, post(EventPriority, Event); on both versions, post call sites whose bus reference cannot be resolved at load time.

FAQ

Will it change the order my listeners run in, or break @Cancelable events?

No. The generated dispatcher visits listeners in exactly the array order the bus produced (which is the priority-sorted order), and cancellation is checked at the same points as in vanilla. Mods that depend on listener ordering or on early-out after cancellation behave identically.

Does it help a pack with only a few mods?

It helps in proportion to how much event traffic you have. A single-player 1.20.1 kitchen-sink pack sees the biggest gain; a nearly vanilla instance still gains a little, because vanilla itself posts events every tick.

Can it conflict with other performance mods?

It does not patch event classes, so observability/optimizer mods that touch the bus are unaffected. If another mod also replaces post call sites, both end up wrapping the same call site; the result is still correct, just with one extra indirection.

Why is this not part of Forge / NeoForge itself?

It needs to rewrite bytecode at load time for every mod, which is not something the loader can do for its own dispatch loop without changing the API contract. As a launch plugin it can do that without touching any API.

Does it work in a modpack?

Yes. Put the correct jar for the pack's game version in mods/. Modpacks are exactly the case where the saving matters, because the number of listeners per event class grows with the mod count.

Links

  • Source, issues and the benchmark harness: GitHub repository linked on this page.
  • License: GNU LGPL 3.0.

Numbers above come from the repository's own benchmark harness on a quiet machine; your mileage depends on your CPU, your modpack and how much event traffic it generates.

Fast Events Team

profile avatar
  • 6
    Followers
  • 9
    Projects
  • 1.9M
    Downloads

More from nutant233View all