寻找有关如何在特定测试中提高 groovy 性能的想法

Looking for ideas on how to improve groovy performance in the specific test

我在将现有批处理过程从 Java 转换为 Groovy 时遇到了一些相当严重的性能问题。在 Java 中编写的现有批处理周期性地运行,从不同的数据源读取数据并执行一些数据 t运行sformation。已发现将 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 为 342ms,Java 为 30ms 以下:

-------------------------------------------------------
 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 中所定义。

更新。

  1. Groovy 代码进行了建议修改:

    List items = (0..length)
    List even = items.findAll { int item -> item > 0 && item % 2 == 0 }
    
  2. 添加了测试方法调用的重复 warm up 测试

I 运行 ./speed-test.sh 分别运行 groovyjava 测试。 jvm 的启动从未包含在测试中。

以下是我在允许预热的同一个 jvm 进程中看到 运行 相同方法 10 次的最佳结果:

/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

正如@blackdrag 指出的那样,Groovy 需要更长的时间来预热。在预热周期之后,它仍然需要大约 5 倍的时间来执行(即使不包括初始预热周期)。更新后的代码在 b运行ch feature/option-1 上,如果有人想查看它。

我有大致的性能测试指南:

  • 确保您测量的时间超过 1 秒,以避免计算机时钟的计时错误。
  • 始终提供 Groovy 和 Java 版本以及计算机规格,以便能够进行比较
  • 总是有足够长的热身阶段
  • 不要运行多个微基准测试在一起
  • 测量多次迭代以获得平均值优于测量单次迭代。

因为性能测试是一个非常广泛的领域,尤其是微基准测试(因为您可能不会测试您认为测试的内容)。我也为您的情况提供了一些提示,但对于这个平台来说,深入所有细节可能太多了。

首先,你应该想好你要测试什么。是峰值性能、平均性能还是初始性能?有或没有启动成本?正如您可能知道的那样,JVM 使用部分解释和部分 运行 时间编译的代码。解释代码何时以及如何转换为编译代码取决于例如调用包含代码的方法的迭代次数(以及使用的类型、代码大小和许多其他因素)

如果您追求最佳性能,那么 junit 不是正确的工具。例如 JMH 在这里会更好,因为它不仅可以处理预热时间,还可以在稳定阶段停止。

例如,在第一次使用 groovy 运行 时间时会完成很多 class 加载,其中会加载默认的 groovy 方法。仅此一项就可以轻松占用您观察的一半时间,而且此时实际上还没有执行任何代码。

@CompileStatic 可以提供帮助,但我们还不能始终阻止加载 groovy 元 class 系统。所以即使这样,也可能会有这种预热成本。更何况JVM本身还有预热成本。

原码在我的电脑上需要752ms左右的原码。添加仅一次迭代的预热,这会减少到 14-20 毫秒。

并且还有一些逻辑上的断开​​...... List items = (0..length).collect()范围已经是一个列表,所以不需要在这里调用collect。这只会通过复制每个元素来生成一个新列表。而 collect() 不会将元素转换为长整数。由于我们正在处理 Integer 对象,因此实际上没有必要通过调用 longValue() 来转换为 long。单独纠正这两件事已经可以将执行时间减少一半(至少在我的计算机上是这样,而且没有预热阶段)。但是热身阶段真的在这里有所作为。因此,通过预热和那些修正,我已经达到 10 毫秒(50k 元素)。为了比较它,Java 版本在这里需要 5ms。我发现已经无法测试它了。因此,如果我用 100 万个元素重做测试,我会看到 73 毫秒 (Java) 与 200 毫秒 (Groovy)。当然,我也更改了 Java 版本以使用 Integer。

添加类型提示以启用原始优化List even = items.findAll {int item -> item > 0 && item % 2 == 0 } 会将性能提高到大约 120 毫秒。

在其他情况下 @CompileStatic 或 运行 使用 invokedynamic(invokedynamic 版本的性能很大程度上取决于 JVM 版本!)也可能有助于提高性能。我想他们不会在这个测试中做太多事情。