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

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

我发现,对于同样简单的任务(遍历1000万个数字,将它们平方,只过滤偶数并减少它们的总和),Java工作得更快。 代码如下:

科特林:

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

Java的:

 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需要85ms,Kotlin需要4470ms才能执行相应的function。 Kotlin慢了52倍。

我怀疑Java编译器会产生优化的字节码,但我不希望看到如此巨大的差异。 我想知道如果我做错了什么? 我怎样才能迫使科特林加快工作? 我喜欢它,因为它的语法,但52倍是一个很大的区别。 而且我只写了类似Java 8的代码,而不是简单的旧版本(我相信它会比给定的更快)。

当你把苹果和橘子进行比较时,结果并没有告诉你很多。 您将一个API与另一个API进行比较,每个API都有完全不同的重点和目标。

由于所有的JDK都与Kotlin特有的附加组件一样多,所以我写了更多的苹果对苹果的比较,同时也考虑到了一些“JVM微基准”问题。

科特林:

 fun main(args: Array) { 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 } 

Java的:

 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 

惊讶? 我也是。

试图弄清楚这种预期结果的倒置,而不是进一步挖掘,我想作出这样的结论:

Kotlin在Iterable上的FP扩展是为了方便。 在95%的使用情况下,您不关心是否需要1或2μs才能在10-100个元素的列表上执行快速映射filter。

Java的Stream API专注于大数据结构上批量操作的性能。 它也提供了实现相同目标的自动并行化(尽管它几乎从来没有实际的帮助),但是由于这些担忧,它的API被削弱了,有时也很尴尬。 例如,许多不会发生并行化的有用操作就不存在了,非终端操作和终端操作的整个范例增加了您写入的每个Streamsexpression式的大小。


我还要谈一些你的发言:

我知道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 } 

Java的:

 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以同样的方式完成了它的工作。

这个问题的假设可能不是很正确:“为什么Kotlin和Java相比如此之慢?

根据我的基准(信贷Marko Topolnik )下面,它可以是一样快或稍快,有点慢或慢得多。

这里是我试过的代码,它测试了以下的实现:

  • 基于java的基于LongStream的实现( 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) { 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)