协议缓冲区(Protocol Buffer)首次使用延迟高

4
在我们的一个Java应用程序中,我们有很多协议缓冲器类,该jar文件本质上公开了一个带有一个方法的接口,另一个应用程序使用该方法。我们注意到第一次调用此方法时,调用时间相当长(> 500ms),而后续调用则快得多(<10ms)。起初我们认为这与我们的代码有关,但在分析过后我们无法证实这一点。通过排除法,显然这与协议缓冲器有关。
当我们尝试在不同的应用程序中执行相同的操作,并且完全不同 - 但也使用协议缓冲器 - 显示出相同的行为时,进一步证实了这一点。此外,我们尝试在启动时创建所有proto buffer类的虚拟实例(XY.newBuilder().build()),并且每次添加每一个实例时,都可以注意到第一次调用的开销会下降。
在.NET中,我可以找到另一个显示类似问题的问题(为什么ProtoBuf在第一次调用时很慢,但在循环内非常快?),但是那里的解决方案似乎特定于C#,使用预编译序列化程序。到目前为止,我还没有在Java中找到相同的问题。是否有类似上述问题中所示的解决方法适用于Java?

2
我猜延迟是因为JVM需要加载新的类。您可以启用类加载日志并查看是否属实。 - talex
这是个好主意,我会尽快尝试启用详细的类加载,并查看是否能证实我的怀疑。 - Yanick Salzmann
1个回答

1
JVM附带了即时编译器(JIT),可以对代码进行大量优化。如果您想进一步了解,可以深入研究JVM内部。其中包括类的加载和卸载、性能分析、代码编译和反编译、偏向锁等。
举个例子,根据this article所述,在OpenJDK中有两个编译器(C1和C2),共有五个可能的代码编译层次:
分层编译具有五个优化层次。它从第0层开始,也就是解释器层,该层使用仪器提供关于性能关键方法的信息。很快,第一级别,也就是简单的C1(客户端)编译器,会对代码进行优化。在第一层,没有任何分析信息。接下来是第二层,只有少数方法被编译(同样由客户端编译器)。在第二层,针对这些少数方法,会收集入口计数器和循环回退分支的分析信息。然后,第三层会看到所有方法都由客户端编译器进行完整的分析信息编译,最后第四层将利用C2服务器编译器。
这里的要点是,如果您需要可预测的性能,每次部署后都应通过运行一些虚拟请求来热身代码。您已经使用虚拟代码创建了所有使用的protobuf对象,但您应该进一步热身实际调用的方法。

我可以通过详细的类加载确认,第一条消息所花费的所有时间都用于类加载。解决方案是使用Reflections包从具有许多protobuf文件的位置获取所有类,现在差异可以忽略不计了。 - Yanick Salzmann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接