正如您所知,Bean Validation 2.0 已于几个月前发布,而 Hibernate Validator 6.0 是其参考实现。

Hibernate Validator 并非唯一一个 Bean Validation 实现,我们还有一个(友好的)竞争对手,称为 Apache BVal

Apollo BVal 尚未赶上 Bean Validation 2.0,但根据Bean Validation 领域的最后一次基准测试是在 2010 年(还记得“Machete don’t text”吗?),我认为是时候重新审视这个基准测试并获取一些新的数据。

特别是我们对 6.0 版本进行的所有优化工作。

参赛者

我们的想法是比较各种 Bean Validation 实现,并展示 Hibernate Validator 6.0 的进步。

我们决定基准测试 3 个实现

  • Hibernate Validator 6.0.4.Final(发布于 2017 年 10 月 25 日)

  • Hibernate Validator 5.4.2.Final(发布于 2017 年 10 月 19 日)

  • Apollo BVal 1.1.2(发布于 2016 年 11 月 3 日)

Hibernate Validator 5.4 和 Apollo BVal 1.1 是 Bean Validation 1.1 的实现,因此在这个基准测试中,我们不会测试 Bean Validation 2.0 的新功能,而只是测试两个规范版本共有的功能。

请注意,Bean Validation 2.0 只添加新功能,不会删除现有的任何功能。

基准测试

单元基准测试

在 Hibernate Validator 中,我们维护一系列 JMH 基准测试,我们可以针对 Bean Validation 实现的各种版本运行这些测试。

这些基准测试非常简单:它们不执行复杂的验证,因为整个目的是测量验证引擎的开销。

对于这个基准测试系列,我们将运行两个不同的基准测试

  • SimpleValidation:我们只是测试了一个具有几个约束的简单Bean的验证。没有太多花哨的地方,只是普通的Bean验证。

  • CascadedValidation:这里的想法是测试级联验证的开销 - Bean只有一个非常简单的约束,但会级联到几个同类型的其他Bean。

Bean验证基准测试

此基准测试来自前面提到的现有基准测试,由Apache BVal团队组合。

我已经将其导入GitHub,使其更加稳定(你可以生成一组随机Bean一次,并为多个基准测试使用相同的Bean集)以及灵活(更容易使用不同的Hibernate Validator实现)。我们可能会将项目置于Hibernate之下,但现在它更像是一个个人项目。

这是一个相当高级的基准测试,因为它生成了一组具有可调整场景的类,然后在这些生成的Bean上运行验证。

它支持诸如分组、继承等功能。

我们使用的默认场景生成200个不同的Bean。

这是Bean验证可能的真实用例的一个相当合理的近似。

一些数字(以及一些精美的图表!)

好的,你来到这里是为了数字和图表,到目前为止,你只是看到了基准测试的介绍。不要离开,它们在这里!

SimpleValidation JMH基准测试

数字以ops/ms为单位,越高越好。

CascadedValidation JMH基准测试

数字以ops/ms为单位,越高越好。

Bean验证基准测试

数字以为单位,越低越好。

结论

Hibernate Validator比以往任何时候都要快:6.0.4.Final比5.4.2.Final和Apache BVal快2到3倍,无论是在单元基准测试还是在更实际的测试中。

两件更了不起的事情

  • 在更现实的基准测试中,结果甚至更好。

  • 在这个过程中,我们还减少了Hibernate Validator的内存占用。

最终,Hibernate Validator 6.0是一个非常推荐的升级,特别是如果你大量使用Bean验证(例如在批处理中)。

我们所做的更改的一些示例

关于我们做出的性能改进,这里有两个示例

  • 初始化的验证器被缓存在全局映射中,并从那里获取。我们保留了此缓存,因为它在你在两个位置之间共享验证器时很有用(例如,当你在不同的属性上有@NotNull约束时),但我们现在也在ConstraintTree中保留了对验证器的引用,从而避免了映射查找。

  • 获取注释的属性相当慢,所以我们现在构建一个存储这些属性一次并一直保存在手边的AnnotationDescriptor。上述映射的键是使用注释及其属性,即使使用hashCode()缓存,我们也有相当大的开销。

请注意,这两个更改单独就带来了30%的速度提升。

内存占用的减少不是这篇博客的主题,但这里有一些我们为了减少Hibernate Validator存储的元数据的大小所做的事情

  • 尽可能减少集合到空/单例集合 - 这在我们的情况下不是微不足道的,因为我们有相当多的这些。

  • 创建静态默认实例以管理默认情况 - 在大多数情况下,您最终会得到一个不包含特定于情况信息的相同对象,识别这种默认情况并重用相同的实例是非常有价值的;

  • 优化非约束性bean/方法/属性的元数据。

请注意,我们的元数据不可变对于优化有很大帮助。

如果您想了解更多关于我们做了什么的信息,您可以查看以下pull请求: 814845868

经验总结

所以,就像任何性能改进工作一样,我们学到了一些经验教训

  • 这是一段有趣的旅程!

  • 创建基准测试,衡量,衡量,再衡量:做某事最快的方法可能不是你所想的。

  • 在不同场景下进行衡量:有时你在某个地方改善了情况,但它在其他地方变得更糟。

  • 反射很慢 - 这不是什么新鲜事 - 但请注意,即使看起来最无害的操作也可能比预期的慢(通常 Method.getParameters() 不是一个简单的getter,它有成本)。

  • 在热点路径上,即使是最轻微的实例创建也可能引入一些不希望的开销。

  • 映射查找的成本远非微不足道。即使缓存了 hashCode()。即使你的 equals() 很快。

  • 大量的微观优化可以带来巨大的改进。

  • 在引入新功能时,在早期阶段对后果进行一些原始的衡量可能是一个好主意。有时,为了非常狭窄的使用案例添加的功能会引入很多开销或使后续优化变得非常复杂。

我们取得了很大的进步,但我们仍然有一个大问题尚未解决:在验证阶段,我们创建了大量的 PathImplNodeImpl 实例,这种情况显然应该得到改善。不幸的是,这并不像听起来那么简单。

引导成本与运行时成本

Bean Validation实现必须收集被验证bean的大量元数据。这通常在引导时完成,以避免在运行时产生这种开销。

在这个基准测试中,我们只关注运行时成本,因为在应用程序的生命周期中,引导成本通常是微不足道的。

为了公平起见,根据我们的观察,Hibernate Validator在引导时通常比Apache BVal慢一些,因为它收集了更多信息。但在实际场景中,您甚至可能不会注意到这一点。

重现这些结果

这些基准测试在一个典型的工程师笔记本电脑上运行(Core i7,16 GB内存)。

如前所述,本文中展示的所有基准测试都是公开可用和开源的,所以您可以自行运行它们

考虑到Bean Validation基准测试的随机性,您可能会得到稍微不同的结果,但我相信它们将突出类似改进。


返回顶部