4个代码生成库的性能比较
在本系列的 第一篇文章中,我们介绍了Java的强类型及动态类型系统 。结论就是这个类型系统让你可以写出表述性强,健壮的应用程序,但是它限制了框架API与用户类型协作的能力。我们还知道了为什么Java的反射API并不总是与用户类型交互的最佳方式。为了将这点解释清楚,我们还分析了一个简单的安全库的实现,它使用了反射API,但却破坏了类型安全,为了保留用户类型,我们使用了代码生成的方式。
在文章的第二部分,我们分析了不同的代码生成库,并重点介绍了一下我自己开发的一个库, Byte Buddy。然后我们基于这个库来实现了一个简单的安全框架。
本文是最后一篇,我们想比较一下不同的库实现之间的性能差别。如果你还没读过前面两部分,最好先看一遍再继续阅读本文。我保证,我会等你看完再继续往下讲(注:这是哄小孩子吗:-))
初识代码生成器
总的来说,好的API并不是一个优秀的代码生成库的唯一条件。代码库的运行时性能可能是一个更重要的因素,尤其是当生成的代码在运行的程序中处于一个比较关键的位置的时候。关于代码生成库的性能,坊间有着诸多传闻,不过我还没找到关于任何一项技术的靠谱的基准测试。
在Java中进行微基准测试并不是一件容易的事情 。如果你要测量一个指定的代码块的执行时间,你通常不知道你测量的到底是什么。Java代码在执行的时候,JIT编译器通常都会介入,最极端的情况下,它可能会擦除掉被测量的代码。
然而在过去的几年里,有几个聪明的家伙想出了一些办法来欺骗JIT编译器,并基于这些想法实现了一些微基准测试的库。我个人最喜欢的是 Java Microbenchmarking Harness,它是随着Open JDK发布的一款工具。
在进行数据测量之前,有必要先回答一个问题:基准测试的目标和关注点是什么?很明显,有些任务使用某个库处理起来可能会更高效些,而另一些任务则可能花的时间就要更长一点。
除此之外,代码生成库通常会牺牲创建类的时间来减少生成类的方法调用的时间。当我们在讨论下面这些数据的时候,应当时刻牢记这点。
看下这些数据吧
在记住我们前面说的东西的同时,我们先来看一个直接比较不同任务的运行时间的JMH基准测试的原始数据。下表中的数据是指每个操作所需要的纳秒数,空格中是采样的标准误差。
Byte Buddy | cglib | javassist | JDK proxy | |||||
使用stub方法实现接口 | 153.800 | (0.394) | 804.000 | (1.899) | 706.878 | (4.929) | 973.650 | (1.624) |
调用子方法 | 0.001 | (0.000) | 0.002 | (0.000) | 0.009 | (0.000) | 0.005 | (0.000) |
继承类调用父类方法 | 172.126 | (0.533) | 1480.525 | (2.911) | 625.778 | (1.954) | - | |
2290.246 | (7.034) | |||||||
调用父类方法 | 0.002 | (0.000) | 0.019 | (0.000) | 0.027 | (0.000) | - | |
0.003 | (0.000) |
第一行显示的是库生成这18个不同接口的空实现所需的时间。在这些生成的运行时类的基础之上,第二行显示的是调用这个生成类实例中的方法所需要的时间。
在这次测试中,Byte Buddy以及cglib的性能最好,因为这两个库你都能将返回值硬编码到生成类中,而javassist和JDK代理都只允许注册一个相应的回调函数。
这样我们可以得出第一个粗略的结论,这就是运行时类的方法实现越具体的话性能越好。听起来显然应该是这样,但其实不然,因为JIT编译器可能会优化这两种方法的性能。
类继承的情况如何
上表中的第三行显示的是继承一个包含18个方法的类所需要的时间。这次并不是创建一个方法存根,而是重写了方法,并调用了它父类的实现。
你可能已经注意到了,Byte Buddy列出了两个测量值,而第二个斜体的数字明显要更大。两个数值代表的是实现父方法调用的两种不同的实现方式。
正如上周所提到的,JVM只允许在同一个实例内进行父方法的调用。因此,调用super方法的最简单的方式就是在拦截方法里进行父方法的调用,这个拦截方法在第一次测试的时候已经实现好了。
但这个方法的灵活性不够,比方说它并不能根据条件来进行调用。为了克服这一限制,Byte Buddy允许你创建一个类似内部类的东西。在本文的前一部分中我们就介绍过了这种方法,在那篇文章中我们生成了一个实现了Callable接口的代理类。
对于任何调用而言,内部类的实例是通过方法中的一个参数所对应的注解注入到拦截方法里的。正如你所看到的,这种创建了一个额外的类的方式,跟其它使用相同策略的库相比,调用super方法所消耗的时间大大减少了。
与此同时,为每个方法生成一个专门的类会带来生成子类的额外开销。cglib和javassist都选择了一种折中的方案来解决这一问题,它们省掉了创建额外类的开销,代价就是每次父方法调用都会增加额外的开销。
结束语:都是为了提升性能
这里有许多值得讨论的东西,不过与此同时,这也是个结束这次代码生成简介的重要时刻。我希望这次概述能帮助你认识到代码生成其实并没有什么神秘的,这并不是只有大型框架才能使用的。有一个顺手的库的话,即使是很小的项目,你也可以使用代码生成来完成切面关注的漂亮的API,而不用增加显式的依赖关系。
现在Java 8已经开始逐渐流行起来,它的新的元空间不再严格限制Java应用包含的类的数量了。有了这些之后,就没有什么能再束缚住你的手脚了,放手去干吧。
原创文章转载请注明出处: 4个代码生成库的性能比较