FastEvent
是一个面向 Forge / NeoForge 的优化模组,优化了一个 Forge 中最基础的功能:事件系统。
有多快
为每一个 FastEvent 支持的 MC 版本写一个测试并不容易,特别是在各版本的事件系统都不一样的情况下。
因此,作者只会提供一份 JMH 性能测试报告,这份报告来自他为 Cleanroom 项目撰写的一份性能优化 PR,其中的性能优化手法与 FastEvent 一样。
注册 10000 个事件监听,发布事件 0 次:
Benchmark Mode Cnt Score Error Units
BusPerformanceTest.register10000Legacy avgt 5 1126.498 ± 284.633 ms/op
BusPerformanceTest.register10000Modern avgt 5 1058.961 ± 173.586 ms/op
大致快了 6.4%。
注册 1000 个事件监听,发布事件 10000 次:
Benchmark Mode Cnt Score Error Units
BusPerformanceTest.register1000test10000Legacy avgt 5 4407.963 ± 4250.643 ms/op
BusPerformanceTest.register1000test10000Modern avgt 5 3550.578 ± 1991.352 ms/op
大致快了 24%。
来源:https://github.com/CleanroomMC/Cleanroom/pull/328#issuecomment-2801099504。
原理
(警告:前有枯燥技术细节)原文如此
当开发者们使用“@EventBusSubscriber”以及“@SubscribeEvent”注册事件监听时,事件系统并不能无中生有地得到事件监听对象,而是只能拿到一个记录了相关信息的“Method”对象。此时最为直接的做法就是直接使用这个对象:“method.invoke(...)”。这样的调用最终会被 JVM 重定向回原来的方法,这样就做到了让事件监听在事件被发布时受到发布的事件。
但是“method.invoke(...)”性能上是非常缓慢的,为了让这个过程更快,Forge 的事件系统会在运行时为每个方法生成一个实现了事件监听功能的类,从而消除了性能代价高昂的反射调用。
但生成类本身也会拖慢性能,为了进一步优化,FaseEvent 将生成类替换为了构造 lambda,从而提高了构造事件监听的速度。另一个好处是 lambda 对应的类是“隐藏”的,这意味着 JVM 不必确保外部可以访问实现事件监听的类的各种数据,因此可以更加彻底地进行优化。
如果恰巧会一点 Java,以下的代码演示或许会更加好懂:
class Listen {
@SubscribeEvent
public void onEvent(Event event) {
}
}
Listen lis = new Listen();
// 原有事件系统会为每一个事件监听生成一个类
class IEventListener$Listen$onEvent implements IEventListener {
private Listen instance;
public IEventListener$Listen$onEvent(Listen instance) {
this.instance = instance;
}
@Override
public void invoke(Event event) {
instance.onEvent(event);
}
}
IEventListener handler = new IEventListener$Listen$onEvent(lis);
// FastEvent 使用 lambda 生成事件监听
IEventListener handler = lis::onEvent;