kotlin与go性能
您可能听说过Graal,这是用Java编写的JVM的新JIT编译器。 自Java10起,它就可以在JDK中使用,将来可能会成为JDK的标准。
如果您有兴趣,可以在这里找到更多信息: https : //www.infoq.com/articles/Graal-Java-JIT-Compiler
去年,我主要与Kotlin合作,作为个人项目,我实现了一个机器人来玩Kotlin中的Go游戏。 您可以在这里找到源: https : //github.com/uberto/kakomu
可以想象,在任何游戏机器人(国际象棋,围棋等)中,计算机播放器的能力都与其速度成正比。 在我的情况下,主引擎基于MonteCarloSearch算法,该算法模拟成千上万的随机游戏以评估每个位置。
那么,为什么不让Graal参加呢?
从Java 10开始,您可以在开始时仅使用VMOptions激活Graal。
首先介绍一下测试方法:
All tests run on my Notebook with i7 2.0Ghz 16Gb Ubuntu 18.4 with OpenJdk 11
graal VM Options:
-Xms6g -Xmx6g -XX:+UseParallelOldGC -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
c2 VM Options:
-Xms6g -Xmx6g -XX:+UseParallelOldGC
I run a test called PerformanceTest in each project which execute a end to end benchmark continuously. In this way the compiler cannot optimize the code for a specific case. Then I choose the faster result, assuming it is the one without GC and OS pauses. All tests are running mono-thread.
结果如下:
如您所见,Graal编译的Kotlin在小板游戏中明显更快,而在大板游戏中则仅稍快一些。
我怀疑这是因为使用更大的板卡内存管理会占用很大一部分运行时间。 无论如何,增加都是令人欢迎的,特别是考虑到我通常在较小的棋盘上比赛。
为了检验我的理论,我创建了一个新项目,其中包含一些经典算法的实现,以查看性能上的差异。 您可以在这里找到它: https : //github.com/uberto/kotlin-perf
目前有两种这样的算法:Mandelbrot集生成器和Knapsack求解器。
曼德布罗集
维基百科参考: https : //en.wikipedia.org/wiki/Mandelbrot_set
Mandelbrot集可能是最著名的分形形状,即使您不知道其名称,您也可能已经看过。
数学上定义为复平面中所有点的集合,其中函数z <-z ^ 2 + c迭代时不会发散。 生成对复杂平面上某些点的函数进行迭代并从中创建图像的集非常容易。
因为这里的目标是性能而不是图形,所以我使用文本图形使事情变得简单。
让我们开始看一下Mandelbrot集的代码。
data class Complex(val r: Double, val i: Double){
operator fun times(other: Complex) =
Complex(
r = this.r * other.r - this.i * other.i,
i = this.i * other.r + this.r * other.i
)
operator fun plus(other: Complex) =
Complex(r = this.r + other.r, i = this.i + other.i)
operator fun minus(other: Complex) =
Complex(r = this.r - other.r, i = this.i - other.i)
fun squared() = this * this
fun squaredModule() = r * r + i * i
fun Double.toComplex() = Complex(r=this, i=0.0)
}
fun mandelSet(initZ: Complex, c:Complex, maxIter: Int): Int {
var z = initZ
(1..maxIter).forEach{
z = z.squared() + c
if (z.squaredModule() >= 4)
return it
}
return maxIter
}
您可以在此处看到如何使用运算符重载和数据类来表示复数如何真正简化代码并使其更易于理解。
一旦我们在Complex类中定义了对复数进行运算的规则,mandelSet函数仅需检查z <-z ^ 2 + c运算是否在“转义”,并且在经过多次迭代后会超过阈值4。
在这里,您可以在AsciiArt渲染的输出中看到Mandelbrot Set的典型心形形状:
背包问题
有关背包问题的维基百科参考: https : //en.wikipedia.org/wiki/Knapsack_problem
背包问题可以用几种方法来定义。 想象一下是一个刚闯入一家手表店的小偷。 只要您不超过背包的最大重量,就可以偷很多手表。
作为一个理性的小偷,您肯定想优化您将带出的手表的价值。 每只手表都有价格和重量。 因此,您需要找到给定重量的总价格最高的手表组。
实际应用包括优化CNC应用的切割和材料,以及用于分配广告预算的营销策略。
例如,让我们从一家只定义了3个手表的商店开始:
val shop = Knapsack.shop(
Watch(weight = 1, price = 1),
Watch(weight = 3, price = 2),
Watch(weight = 1, price = 3)
)
如果我们的最大重量为1,则最好拿起第三只手表而不是第一只,因为该值更高。
如果最大权重为3,则可以选择数字2(价格2)或数字1和3(价格1 + 3)。 在这种情况下,即使它们的总重量小于最大重量,也最好捡起1和3。
这些是该商店的完整解决方案:
assertEquals(3, selectWatches(shop, maxWeight = 1))
assertEquals(4, selectWatches(shop, maxWeight = 2))
assertEquals(4, selectWatches(shop, maxWeight = 3))
assertEquals(5, selectWatches(shop, maxWeight = 4))
assertEquals(6, selectWatches(shop, maxWeight = 5))
如您所见,随着可用手表数量的增加,可能的选择数量非常非常快地增加。 这是一个经典的NP-Hard问题。
为了在合理的时间内解决它,我们需要作弊并使用动态编程。 我们可以使用已经针对每组手表进行了优化的解决方案来构建地图,因此我们可以避免每次都重新计算它们。
通用算法基于基于递归的穷举搜索。 这是解决该问题的Kotlin代码,分为备忘录功能和最大值的递归搜索。
typealias Memoizer = MutableMap<String, Int>
fun priceAddingElement(memo: Memoizer, shop: Set<Watch>, choice: Set<Watch>, maxWeight: Int, priceSum: Int): Int =
shop.filter { !(it in choice) && it.weight <= maxWeight }
.map {
selectWatches(
memo,
shop,
maxWeight - it.weight,
choice + it,
priceSum + it.price) }
.filter { it > priceSum }
.max() ?: priceSum
fun selectWatches(memo: Memoizer, shop: Set<Watch>, maxWeight: Int, choice: Set<Watch>, priceSum: Int): Int =
memoization(memo, generateKey(choice)) {
priceAddingElement(memo, shop, choice, maxWeight, priceSum)}
private fun memoization(memo: Memoizer, key: String, f: () -> Int): Int = when (val w = memo[key]) {
null -> f().also { memo[key] = it }
else -> w
}
我非常喜欢Kotlin如何允许表达自己的意图而不必重复自己。 如果您不了解Kotlin,我希望这段代码能引起您的兴趣,有一天可以尝试一下。
基准测试
现在,您一直在等待的部分。 让我们比较一下Graal和旧的C2编译器的性能。
让我们记住Graal是用Java编写的,它利用了编译器领域的新研究,但是它仍然相对较年轻。 另一边的C2调优和成熟。
第一个惊喜是关于Mandelbrot的示例:
老实说,我没想到会有如此大的性能差异。 Graal比C2快18%! 只是要确保我尝试使用具有相同结果的略有不同的公式。 Graal确实非常适合编译Kotlin中进行的计算。
这里的Graal慢了54%!
做一些分析后,我发现我的代码大部分时间都花在了生成用于记忆的键的函数上。
为确保正确,我对集合进行了排序,然后转换为字符串。 这是很多不必要的工作,它依赖于HashSet java实现。
因此,我更改了从中生成密钥的方法:
private fun generateKey(choice: Set<Watch>): String = choice.sortedBy { "${it.price}-${it.weight}" }.toString()
对此:
private fun generateKey(choice: Set<Watch>): String =
choice.map{ it.hashCode() }.sorted().joinToString("")
新功能更快,因为它可以对Watches的哈希(唯一)进行排序,然后将它们连接到String中。
请注意,我们不能简单地使用Set的哈希值,因为可能会发生哈希冲突。 我实际上尝试过并验证它开始发出错误的结果。
可以为Set创建一个更安全的哈希方法,但是这里的目标不是最大程度地优化算法,而是编写有效且清晰的Kotlin代码。
现在,常见的Kotlin的结果是:
在这里,Graal再次明显比C2快,并且总体而言,新的密钥生成器比以前的实现快得多。
我对这些结果的猜测是,C2已针对典型的Java使用进行了严重优化(使用内部函数等),而Graal擅长于编译惯用的Kotlin所采用的小型方法和轻量级对象。
所有这些都值得进一步研究,但我希望本文能激发更多Kotlin开发人员使用Graal。
如果您对更多类似的帖子感兴趣,请关注我的Twitter帐户@ramtop或查看我的博客
翻译自: https://www.javacodegeeks.com/2018/12/comparing-kotlin-performance-graal-c2.html
kotlin与go性能