为什么Kotlin的map-filter-reduce比大型输入上的Java Stream操作慢?

时间:2018-01-18 09:22:57

标签: java kotlin

几天前我创建了一个简单的基准测试(没有jmh和所有其他专门的东西,只是为了粗略测量)。

我发现,对于同样简单的任务(遍历1000万个数字,对它们进行平方,仅过滤偶数并减少它们的总和),Java的工作速度要快得多。这是代码:

科特林:

fun test() {
    println((0 .. 10_000_000L).map { it * it }
                              .filter { it % 2 == 0L }
                              .reduce { sum, it -> sum + it })
}

爪哇:

public void test() {
    System.out.println(LongStream.range(0, 10_000_000)
                                 .map(it -> it * it)
                                 .filter(it -> it % 2 == 0)
                                 .reduce((sum, it) -> sum + it)
                                 .getAsLong());
}

我正在使用Java版本1.8.0_144和Kotlin版本1.2。

在我的硬件上,Java平均需要 85 ms,而Kotlin执行相应功能需要 4,470 ms。 Kotlin工作速度慢了52倍。

我怀疑Java编译器产生了优化的字节码,但我没想到会看到如此巨大的差异。我想知道我做错了什么?我怎样才能迫使Kotlin更快地工作?我喜欢它,因为它的语法,但52次是一个很大的区别。我只是编写了类似Java 8的代码,而不是普通的旧迭代版本(我相信,它会比给定版本快得多)。

2 个答案:

答案 0 :(得分:28)

当您将苹果与橙子进行比较时,结果并不能说明问题。您将一个API与另一个API进行了比较,每个API都有完全不同的焦点和目标。

由于所有JDK都与Kotlin特定的添加一样多“Kotlin”,我写了更多的苹果对苹果比较,这也解决了一些“JVM微基准”问题。

科特林:

fun main(args: Array<String>) {
    println("Warming up Kotlin")
    test()
    test()
    test()
    println("Measuring Kotlin")
    val average = (1..10).map {
        measureTimeMillis { test() }
    }.average()
    println("An average Kotlin run took $average ms")
    println("(sum is $sum)")
}

var sum = 0L

fun test() {
    sum += LongStream.range(0L, 100_000_000L)
            .map { it * it }
            .filter { it % 2 == 0L }
            .reduce { sum, it -> sum + it }
            .asLong
}

爪哇:

public static void main(String[] args) {
    System.out.println("Warming up Java");
    test();
    test();
    test();
    System.out.println("Measuring Java");
    LongSummaryStatistics stats = LongStream.range(0, 10)
                                            .map(i -> measureTimeMillis(() -> test()))
                                            .summaryStatistics();
    System.out.println("An average Java run took " + stats.getAverage() + " ms");
    System.out.println("sum is " + sum);

}

private static long sum;

private static void test() {
    sum += LongStream.range(0, 100_000_000)
                     .map(it -> it * it)
                     .filter(it -> it % 2 == 0)
                     .reduce((sum, it) -> sum + it)
                     .getAsLong();
}

private static long measureTimeMillis(Runnable measured) {
    long start = System.nanoTime();
    measured.run();
    return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
}

我的结果:

Warming up Kotlin
Measuring Kotlin
An average Kotlin run took 158.5 ms
(sum is 4276489111714942720)


Warming up Java
Measuring Java
An average Java run took 357.3 ms
sum is 4276489111714942720

惊讶?我也是。

我不想进一步挖掘,试图找出预期结果的这种反转,我想得出这样的结论:

Iterable上的Kotlin FP扩展是为了方便起见。在95%的用例中,您不关心在10-100个元素的列表上执行快速映射过滤器需要1或2μs。

Java的Stream API专注于大型数据结构上批量操作的性能。它还提供了针对同一目标的自动并行化(虽然它几乎从未真正帮助过),但由于这些问题,它的API已经瘫痪并且有时很尴尬。例如,许多没有实现并行化的有用操作就不存在了,非终端操作和终端操作的整个范例为您编写的每个Streams表达式增加了批量。

我还要谈谈你的一些陈述:

  

我知道Java编译器会生成优化的字节码

这是a)不正确和b)在很大程度上无关紧要,因为(几乎)没有“优化字节码”这样的东西。解释的字节码执行总是比JIT编译的本机代码慢至少一个数量级。

  

我刚刚编写了类似Java 8的代码,而不是简单的旧迭代版本(我相信,它会比给定版本快得多)。

你是说这个?

科特林:

fun test() {
    var sum: Long = 0
    var i: Long = 0
    while (i < 100_000_000) {
        val j = i * i
        if (j % 2 == 0L) {
            sum += j
        }
        i++
    }
    total += sum
}

爪哇:

private static void test() {
    long sum = 0;
    for (long i = 0; i < 100_000_000; i++) {
        long j  = i * i;
        if (j % 2 == 0) {
            sum += j;
        }
    }
    total += sum;
}

结果如下:

Warming up Kotlin
Measuring Kotlin
An average Kotlin run took 150.1 ms
(sum is 4276489111714942720)

Warming up Java
Measuring Java
An average Java run took 153.0 ms
sum is 4276489111714942720

在这两种语言中,性能几乎与上面的Kotlin + Streams API相同。如上所述,Streams API针对性能进行了优化。

在这个简单的源代码中,kotlincjavac可能产生了非常相似的字节码,然后HotSpot以同样的方式完成了它的工作。

答案 1 :(得分:3)

可能这个问题的假设不太正确:&#34;为什么Kotlin与Java相比这么慢?&#34;

根据我的基准(Marko Topolnik的信用额度),它可以更快或更快,更慢或更慢。

以下是我尝试过的代码,用于测试以下实现:

  • 基于java LongStream的实现(尽快)
  • 使用Kotlin的sequence。 (慢了5倍左右)
  • 在问题中使用的模式之后没有使用序列(慢得多)

...

import java.util.stream.LongStream
import kotlin.system.measureTimeMillis

var sum = 0L

val limit = 100_000_000L

val n = 10

fun main(args: Array<String>) {
    runTest(n, "LongStream", ::testLongStream)
    runTest(n, "Kotlin sequence", ::testSequence)
    runTest(n, "Kotlin no sequence", ::testNoSequence)
}

private fun runTest(n: Int, name: String, test: () -> Unit) {
    sum =  0L
    println()
    println(":: $name ::")
    println("Warming up Kotlin")
    test()
    test()
    test()
    println("Measuring Kotlin")
    val average = (1..10).map {
        measureTimeMillis { test() }
    }.average()
    println("An average Kotlin run took $average ms")
    println("(sum is $sum)")
}

fun testLongStream() {
    sum += LongStream.range(0L, limit)
            .map { it * it }
            .filter { it % 2 == 0L }
            .reduce { sum, it -> sum + it }
            .asLong
}

fun testSequence() {
    sum += (0 until limit).asSequence().map { it * it }
            .filter { it % 2 == 0L }
            .reduce { sum, it -> sum + it }
}

fun testNoSequence() {
    sum += (0 until limit).map { it * it }
            .filter { it % 2 == 0L }
            .reduce { sum, it -> sum + it }
}

当您运行上面的代码时,您将在控制台上看到此输出 - 它可以让您了解Kotlin可以获得的性能多样性:

:: LongStream ::
Warming up Kotlin
Measuring Kotlin
An average Kotlin run took 160.4 ms
(sum is 4276489111714942720)

:: Kotlin sequence ::
Warming up Kotlin
Measuring Kotlin
An average Kotlin run took 885.1 ms
(sum is 4276489111714942720)

:: Kotlin no sequence ::
Warming up Kotlin
Measuring Kotlin
An average Kotlin run took 16403.8 ms
(sum is 4276489111714942720)