在将现有批处理流程从Java
转换为Groovy
时,我遇到了一些相当重要的性能问题。用Java
编写的现有批处理过程定期读取来自不同数据源的数据并执行一些数据转换。已发现的是,在将Java
代码转换为Groovy
后,性能显着降低,出现意外的高差距10倍以上。
https://github.com/nicolas-martinez/grava-speed-test处的代码是一个简化示例,它显示了使用简单循环发现的问题之一以及使用集合闭包进行过滤。它设置为Maven
项目,可以在本地轻松克隆并执行。
以下是Groovy代码的重点:
List items = (0..length).collect()
List even = items.findAll { item -> item > 0 && item.longValue() % 2 == 0 }
和Java代码:
List<Long> items = new ArrayList(length);
for (int i = 0; i < length; i++) {
items.add(Long.valueOf(i + 1));
}
List<Long> even = new ArrayList<Long>();
for(Long item : items){
if (item > 0 && item % 2 == 0) {
even.add(item);
}
}
Groovy
测试结果为342毫秒,Java
测试结果为30毫秒:
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running tst.speedtest.GroovyFilterTest
testFilter: 500000 elapsed: 342
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.637 sec
Running tst.speedtest.JavaFilterTest
testFilterUsingInterface: 500000 elapsed: 29
testFilter: 500000 elapsed: 27
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.048 sec
如果您对如何提高Groovy性能有任何建议,请与我们联系。我们的团队正在考虑迁移到Groovy,因为它提供了一些先进的功能,但由于迄今为止我们遇到的性能差距很大,因此很难证明这一点。
以下是system_profiler SPHardwareDataType
报告的我的硬件配置文件:
Hardware Overview:
Model Name: MacBook Pro
Model Identifier: MacBookPro11,3
Processor Name: Intel Core i7
Processor Speed: 2.5 GHz
Number of Processors: 1
Total Number of Cores: 4
L2 Cache (per Core): 256 KB
L3 Cache: 6 MB
Memory: 16 GB
Boot ROM Version: MBP112.0138.B11
SMC Version (system): 2.19f12
这是Java
版本:
java version "1.7.0_72"
Java(TM) SE Runtime Environment (build 1.7.0_72-b14)
Java HotSpot(TM) 64-Bit Server VM (build 24.72-b04, mixed mode)
Groovy
版本为2.3.7
pom.xml中定义的Groovy
。
更新。
对List items = (0..length)
List even = items.findAll { int item -> item > 0 && item % 2 == 0 }
代码建议修改:
warm up
重复测试方法调用./speed-test.sh
测试
我运行了groovy
,它分别运行java
和jvm
个测试。 /speed-test.sh
Java test
Java testUsingInterface: 500000 elapsed: 44
Java testUsingInterface: 500000 elapsed: 43
Java testUsingInterface: 500000 elapsed: 28
Java testUsingInterface: 500000 elapsed: 11
Java testUsingInterface: 500000 elapsed: 31
Java testUsingInterface: 500000 elapsed: 10
Java testUsingInterface: 500000 elapsed: 9
Java testUsingInterface: 500000 elapsed: 11
Java testUsingInterface: 500000 elapsed: 19
Java testUsingInterface: 500000 elapsed: 19
JavaTest: for testSize=1000000 and repeat=10 total elapsed: 226
Groovy Test
GroovyTest: 500000 elapsed: 199
GroovyTest: 500000 elapsed: 76
GroovyTest: 500000 elapsed: 91
GroovyTest: 500000 elapsed: 80
GroovyTest: 500000 elapsed: 58
GroovyTest: 500000 elapsed: 83
GroovyTest: 500000 elapsed: 91
GroovyTest: 500000 elapsed: 58
GroovyTest: 500000 elapsed: 58
GroovyTest: 500000 elapsed: 67
GroovyTest: for testSize=1000000 and repeat=10 total elapsed: 1073
的启动从未包含在测试中。
以下是我能够在同一个jvm进程中运行相同方法10次以获得预热的最佳结果:
Groovy
正如@blackdrag指出的那样,feature/option-1
需要更长的时间来预热。在预热循环之后,执行仍然需要约5倍(即使排除了初始预热循环)。如果有人想要查看,则更新的代码位于分支{{1}}上。
答案 0 :(得分:6)
我大致有一些性能测试指南:
由于性能测试是一个非常广泛的领域,尤其是微基准测试(因为您可能无法测试您认为测试的内容)。我也为你的案例提供了一些提示,但是对于这个平台来说,进入所有细节可能太过分了。
首先,您应该考虑要测试的内容。它是峰值性能,平均性能还是初始性能?有没有启动成本?您可能知道JVM使用部分解释的和部分运行时编译的代码。何时以及如何将解释的代码转换为编译代码取决于例如迭代次数,调用包含代码的方法(以及使用的类型,代码大小和许多其他内容)
如果你达到最佳表现,那么junit不是正确的工具。例如JMH在这里会更好,因为它不仅可以处理预热时间,还可以在稳定阶段停止。
例如,在第一次使用groovy运行时时会执行很多类加载,其中加载了默认的groovy方法。仅此一项就可以轻松占用您观察到的一半时间,实际上此时尚未执行任何代码。
@CompileStatic
可以提供帮助,但我们还不能总是阻止加载groovy元类系统。因此即使这样,也可能有这种预热成本。更不用说JVM本身就有预热成本。
我的计算机上的原始代码需要大约752ms的原始代码。只添加一次预热的预热,这可以降低到14-20ms。
还有一些逻辑断开连接...... List items = (0..length).collect()
范围已经是一个列表,所以不需要在这里调用collect。这只会通过复制每个元素来生成一个新列表。而collect()不会将元素转换为long。由于我们处理的是Integer对象,因此无需通过调用longValue()
将其转换为long。仅纠正这两件事就已经将执行时间减少到一半(至少在我的电脑上,没有预热阶段)。但是热身阶段在这里真的有所作为。因此,通过预热和那些修正,我已达到10ms(50k元素)。为了比较它,Java版本需要5ms。我发现它已经很短暂来测试它。因此,如果我用100万个元素重做测试,我会看到73ms(Java)与200ms(Groovy)。当然我也改变了Java版本以使用Integer。
添加类型提示以启用原语优化List even = items.findAll {int item -> item > 0 && item % 2 == 0 }
可将性能提升至约120毫秒。
在其他情况下@CompileStatic
或使用invokedynamic运行(invokedynamic版本的性能在很大程度上取决于JVM版本!)也可能有助于提高性能。在我假设的这个测试中,他们不会做太多。