随着对 Kotlin 越来越深入的了解,我发现市面上关于 Kotlin 方面,比较深入的资料几乎是 0,所以我决定,将 Kotlin 各个方面的研究作为我的研究生课题,而性能问题往往是程序员最佳关注的内容,所以第一篇,我决定先对比一下 Java 和 Kotlin 之间的性能,通过著名的计算机语言的基准测试游戏来对两者的性能做一个全方位的对比。
大约一年前,我还是一名 Poznań 大学的大学生,学习软件工程相关的内容,考虑硕士论文相关的内容。几乎所有推荐学习的内容都超级无聊,我对它们提不起任何的兴趣,所以决定自己做一些研究课题。
与此同时,10 月份,我和 RSQ 技术组织的朋友一起前往阿姆斯特丹参加 KotlinConf 2018 的技术峰会。与来自 Kotlin 社区的很多有趣的朋友,一起参加了这场封闭式的座谈会。这其中就有德克斯大学的教授 Wiliam Cook,他在会中提到,Kotlin 在科学界没有太多的读者,关于 Kotlin 相关的论文也不多。这个时候我内心默默地问自己——是不是可以做点什么,试着写一些与 Kotlin 相关的内容?
会议结束的几天之后,我在家里听着旧的 KotlinConf 演讲内容,发现了 Duncan McGregor 提出的,关于缩短 Kotlin 代码大小和缩短执行时间的演讲。他点醒了我,让我更加深入地研究性能相关的问题,并开始在 Java 和 Kotlin 之间进行对比。
几个月后的现在,我作为一名研究生,在这里试图通过 Medium 平台的这篇文章,来展示我的研究成果,希望你能喜欢并能从中获益。
研究问题
在文章开始之前,我想说我的研究生论文中,研究的主要有两个问题,但是在这篇文章里面,我先介绍其中的一个——性能相关的问题。
问题:在相同的基准测试内容下,不同的 Java 运行时环境(JRE)对于性能会有显著的差异么?
计算机语言的基准测试游戏
动态度量指标,是出自于目前比较流行的跨语言基准测试套件之一—— 计算机语言基准测试游戏(CLBG)的思路。
由 Doug Bagley 在 2000 年推出的,致力于对比所有编程语言的计算机项目。直至今天,该项目已经成为了计算机语言的基准测试的一个游戏,也是科学界比较流行的跨语言基准测试。随着新的基准测试诞生以及语言实现的增长而不断成长。创建者会系统地进行更新和整理,来达到跟踪市场趋势的目的(添加新的语言,删除不再使用的语言,并更新要测试的基准测试算法列表)。
GLBG 基准测试背后,有一个目标,它的内容与下面这个,来自 4chan 平台用户提出的问题的答案基本一致:
我的问题是,如果在座有人有过简单的基准测试的经验,能告诉我应该测试哪些内容,以便对每种语言的总体性能有一个简单的了解吗?
为了回答这个问题,GLBG 团队提出了 10 种不同的算法问题。所有的问题以及相关的细节都可以在官网上看到。对于如何实现这些算法也有较为严苛的标准,算法最后的结果用什么去对比也是一个问题。这些信息确定之后,相关的问题就可以用指定的语言去实现,然后进行判断。
为了能够客观的评价结果,CLBG 基准测试组使用固定的脚本内容,对待所有的实验统一实现的指标。对于所有指定的语言,实现算法使用的测试值都是独立的。
基准程序的选择
随着实现内容的不断开发,计算机语言基准测试游戏目前由 10 个基准程序组成(备注:这个数字随着时间也会不断增加)。每一个都提供了一个完全不同的问题,针对这个问题,使用不同的语言规范、语言特性和方法,试图使用最通用的方式来解决它们。(我不打算介绍 CLGB 每一个基准的内容,如果你有兴趣的话,可以到他们的官网查询相关的细节)
我们实验的主要目的还是比较 Java 和 Kotlin,为了达到这一点,我们选用一个取自 CLBG 基准库里面的、由 Java 实现的程序。然后使用 Kotlin 实现两个版本 —— 一个是单纯的转换版本,另一个是针对问题使用 Kotlin 的惯用版本。(在下一节会详细介绍)
基于这些假设,我们根据两个因素来选择实验中使用的基准:
从 CLBG 存储库中获得的最合适的 Java 程序必须可转换为 Kotlin 语言。
程序必须对尽可能多的数据进行操作。
《JVM 托管语言:是否真的说到做到》的论文作者提出了一种区分 CLBG 程序库的区分方法,主要的判断条件是,程序主要操作的是整数、浮点数、指针还是字符串来区分。这些信息有助于我们将基准进行分组。
考虑到以上全部的因素,只有 6 个 GLBG 的基准测试是可行的。为了达成在代码修改量足够小的前提下,Java 代码必须可以很轻易转换成 Kotlin 语言版本这个条件,10 个基准测试中的 4 个被排除掉了。
int - integer
fp - floating point
ptr - pointer
str - string
表 1: 选择基准测试,其中包含关于大多数操作数据的信息
备注
在基准选择之后(如表 1 所示),最终的完整基准测试组件都不包含主要对字符串资源进行操作的程序。
代码实现
每一个基准测试都包括如下三个实现的版本:
Java 版本
Kotlin 转换的版本
Kotlin 惯用的实现版本
所有的代码都会用于实验——通过外部的 Python 脚本编译、执行和测试。里面没有添加任何有可能影响结果的实现代码。
Kotlin 里面分成了两个版本,主要是我想看一下不同场景下的 Kotlin 版本对结果的影响。单纯转换过后的代码可能跟 Java 的性能更为接近,字节码的结果也更相似。不过从另一个方面考虑,使用 Kotlin 惯用的实现版本,更能够体现出经验丰富的 Kotlin 程序员代码的对比结果。
如果你对实现的细节感兴趣的话,可以检出 Java vs Kotlin的对比仓库。
Java 实现
所有 Java 代码都直接取自计算机语言基准测试游戏库、大多数人使用的实际基准测试实现内容,并没有对代码进行任何的修改。
在我们的基准测试存储库中,有多个版本的 Java 基准测试。根据 CLBG 网页上的排行榜,实验中使用的代码是性能最优的代码。
Kotlin 转换的实现版本
实现的代码是使用 Java 转 Kotlin 工具帮助完成的,工具由 IntelliJ IDEA 提供。对原始的代码进行了一定量的修改,保证代码可以执行。所有的修改都是建立在能够让编译器执行代码的条件下的,所以都是有必要的修改。
所有 Kotlin 转换的实现代码都加入了一个特殊的修改。为了使用命令行接口帮助我们编译代码,我们把 main()方法提取到了原始文件的外面。
Kotlin 惯用的实现版本
对于 Kotlin 惯用版本的实现,所有修改的内容介绍,都基于以下的几点:
惯用程度 —— 为了列举 Kotlin 常用的习惯列表,这里使用的链接是 Kotlin 官方文档。
代码的转换情况 —— 其中包含当前 Kotlin 语言推荐的编码风格。该页面也使用了 Kotlin 文档的一部分。
IDEA(编译器)默认的代码检查规则。
Kotlin 的惯用版本是基于转换后的版本来处理的。
备注
六个基准测试中有五个使用线程并行工作(Java 和 Kotlin 实现),Kotlin 实现中没有一个使用协程,因为使用协程可能会显著影响性能结果
语言版本及硬件
这里有必要记录一下执行基准测试时的软件和硬件环境。
执行实验的硬件信息我放在表 2 里面了。之所以选择 Linux Ubuntu 系统,是因为它被推荐用于 CLBG 测量脚本的操作系统。
表 3 给出了用于执行基准内容的 Java 和 Kotlin 版本。这两个版本是目前可用的最新版本。
备注
所有的基准测试都是在 Oracle HotSpot VM 上执行的。
动态指标
那么,我应该使用那些指标来进行对比呢?在这里我决定使用大多数最常用的指标来进行对比:
执行时间
内存使用情况
CPU 使用情况
每个程序都被执行和测量了 500 次。
基准测试指标也基于计算机语言基准测试游戏中使用的指标。所有程序都使用专用CLBG脚本执行和测量。
在 Kotlin 和 Java 基准测试组件开发过程中试验了多种测量的方法,并且这些内容都存在了代码仓库里面。最初,使用了 Java/Kotlin 代码和 System 类中的 currentTimeMilis()或 nanoTime()等方法来测量时间,但是在后期的工作中放弃了这个想法。因为结果差异很大,后来我决定放弃这种测量方法。测量其他负载指标(如 CPU 和内存)也不是很简单,由于环境的原因会受各种因素的影响(深入研究基准测试方法是一个很长的话题,在此我不想做过多阐述)。
在尝试过所有的方法过后,我们决定在这种情况下,使用官方的 CLBG 测量脚本,因为这个方法最客观。该方法也可以帮助我们,将所有的 Kotlin 测量结论放在 CLBG 给出的、不同语言的基准评估结果当中。
有关 Python 脚本和如何测量每个参数的详细信息,请参阅CLBG页面。
结果
项目仓库中,记录了每个基准测试的所有测量结果。
执行时间
下图显示了每个基准测试与每个实现的执行时间。括号中的字母表示大多数被使用的数据类型:
内存使用情况
下图显示了每个基准测试与每个实现的内存消耗情况。括号中的字母表示大多数被使用的数据类型:
CPU 占用
下表显示了每个基准测试与每个实现中,每个 CPU 核上的 CPU 使用情况:
结论
下表给出了每个基准测试结果,内存消耗的中位数和执行时间中位数的对比结果。括号中的字母代表大多数被使用的数据类型:
首先值得注意的是,kotlin 惯用的实现方式,没有在任何测量的结果中获胜。在本例中,执行时间的中位数要高于其他的实现。内存消耗也是如此:惯用的实现方式在某些场景下会处于第二的位置,但是从来没有在给定的基准测试中获得第一的结果。因此,我们可以得出结论,对于希望在内存管理和最快执行时间方面获得最佳结果的人来说,使用推荐的技术编写方式,写出来的 Kotlin 代码可能不是最好的。
基于以上的结果,我们还可以假设,Java 的实现方式,通常更善于管理内存优化和程序执行。在执行时间上,在 6 个基准测试中 Java 的实现方式有 4 个是最优的。与此同时也在 6 个基准测试中有 4 个达到了最好的内存使用。kotlin 转换的版本,在 Mandelbrot 和 Fannkuch Redux 的测试中,执行时间更短,在 Fasta 和二叉树基准测试中内存消耗更低。
在二叉树基准测试中,最高与最低的中位数上,执行时间上的差异是显而易见的。最佳执行时间(Java 版本)和最差执行时间(Kotlin 惯用的版本)之间的差值为 6.76%。在一些基准测试中,Java、kotlin 转换版本和 Kotlin 惯用实现三个版本,达到了非常相似的结果。在谱范数和曼德尔布罗特中,最高和最低中位数执行时间的差异小于 1%。
在 Fannkuch Redux 基准测试中,可以看到相当大的内存使用差异。如前所述,Fannkuch Redux 是一个主要处理整数的程序。Java 和 Kotlin 管理整数的方式有本质上的差异。在 Java 中,没有小数部分的数字大多保存为基本数据类型,而 Kotlin 中所有这些数字都必须封装为整数对象。
在图 3 中,我们可以看到,与其他实现相比,kotlin 转换的实现版本,内存消耗的中位数要低得多。内存使用中位数之间的差异甚至大于 100MB。本文的研究并没有对这个结果给出清晰的解释,因为即使静态字节码分析结果也没有显示出任何有利于 kotlin 转换版本的证据,能提供这么明显的差异。
另一方面,我们还有最后一个测量指标 —— CPU 负载。正如上一节所提到的,在特定的测量值之间没有显著的差异。考虑到任务是随机分配给 CPU 内核的,在大多数情况下,实现之间的差异不超过 3%。因为计算机硬件和软件的不同,在不同的执行级别上,差异也是不一样的。用不同语言实现的基准测试之间的差异可能会大得多。在官方的 CLBG 基准测试页面上,可以查看到相关的 CPU 负载结果的情况,它们之间的差异更大。
凭借这些信息,我们可以假设,这两种语言都以类似的方式去加载 CPU。因为在上面的语言和实现方式的对比上,CPU 负载没有显著降低。
那么我们研究问题的答案又是什么呢?
在这篇文章开始的时候,我提出了我的研究问题。写到这里,应该基于已取得的成果来整理一下答案了。
问题:在相同的基准测试内容下,不同的 Java 运行时环境(JRE)对于性能会有显著的差异么?
从执行时间的角度来看,最高和最低的中位数,在运行时间之间的最大差异可以在 6.7%(支持 Java 实现二叉树基准测试)到 1.2%(支持 kotlin 转换的实现 Fannkuch Redux 基准测试)之间。Kotlin 的惯用实现版本没有在一个基准测试中达到最佳的执行时间。 Java 代码在 6 个基准测试中有 4 个达到了最佳时间的中位数,其余的基准测试在 kotlin 转换的代码中执行得最好。
在六个基准测试中,Java 在其中的四个测试中,内存消耗最低。另两个是 kotlin 转换代码的表现更好。这两种实现的最佳内存消耗结果不会与这些代码的最佳执行时间结果重叠。内存管理可以在不同的基准测试中,随着实现语言的不同而变化。在 Fannkuch Redux 中,Java 实现实现了更好的内存使用,甚至达到 13%,但另一方面,与 Java 结果相比,kotlin 转换的二叉树性能提高了 9.7%。
CPU 负载测试结果中,这两种语言在 CPU 负载方面没有显著差异。所有的基准实现都实现了非常相似的结果。
接下来的任务
测量的指标是动态的。我不是一个统计和数据分析方面的专家,所以在这些结果中可能还包括了大量的其他结论,但是我并没有一一提出。如果有人对此感兴趣—— 我希望能帮助我从我的数据中,分析出更多的内容。
另外,如果您在我的对比中发现了任何的错误,比如说:错误的实现、中位数的评估等等 —— 请在评论区,Twitter或官方的Kotlin slack (Jakub Aniola)中联系我。
在下一部分中,我将介绍“Java vs Kotlin”静态分析的方法和结果。我认为下次我们应该从不同的角度来看看这两种语言之间的差异,从 JVM 字节码的视角。
原文链接:
评论