首先,让我们谈谈测试方法:
所有 测试 都 在 我的 笔记本 上 运行i7 2.0 Ghz 16 Gb Ubuntu 18.4 和 OpenJdk 11
graal VM 选项:
- Xms6g - Xmx6g - XX:+ UseParallelOldGC - XX:+ UnlockExperimentalVMOptions - XX:+ UseJVMCICompiler
c2 VM 选项:
- Xms6g - Xmx6g - XX:+ UseParallelOldGC
我 运行 一个 测试 称为 PerformanceTest 在 每个 项目中 ,其 执行 一 结束 ,以 结束 标杆 不断。在 这种 方式 的 编译器 不能 优化 的 代码 为 一个 特定的 情况下。于是 我 选择 了 更快的 结果,假设 它 是 在 一个 没有 GC 和 OS 暂停。所有的 测试 均 运行 单- 线程。
以下是结果:
正如你所看到的,Graal使用Kotlin进行编译在小板上的速度明显更快,并且随着大板随机播放的速度稍快一点。
我怀疑这是因为随着更大的主板内存管理,它占用了大量的运行时间。在任何情况下,增量都是受欢迎的,特别是考虑到我通常在较小的板上玩。
为了测试我的理论,我创建了一个新项目,其中包含一些经典算法的实现,以查看性能的差异。你可以在这里找到它。
目前,有两种算法:Mandelbrot Set生成器和Knapsack解算器。
Mandelbrot集
该Mandelbrot集可能是你见过的最有名的分形-即使你不知道这个名字。
在数学上,它被定义为复平面中所有点的集合,其中函数z < - z ^ 2 + c在迭代时不会发散。生成Set迭代复杂平面上某些点的函数并从中创建图像非常容易。
由于这里的目标是性能而不是图形,我使用文本图形保持简单。
让我们从查看Mandelbrot Set的代码开始。
数据 类 Complex(val r:Double,val i:Double){
操作员 欢乐 时光(其他:复杂)=
复杂(
r = 这个。r * 其他。r - 这个。我 * 其他。我,
我 = 这个。我 * 其他。r + 这个。r * 其他。一世
)
操作员 乐趣 加(其他:复杂)=
复杂的(- [R = 此。[R + 等。[R ,我 = 此。我 + 其他。我)
操作员 乐趣 减去(其他:复杂)=
复杂的(- [R = 此。[R - 其他。ř,我 = 此。我 - 其他。我)
fun squared()= this * this
有趣的是 squaredModule()= r * r + i * i
有趣的 双。toComplex()= Complex(r = this,i = 0.0)
}
有趣的 mandelSet(initZ:Complex,c:Complex,maxIter:Int):Int {
var z = initZ
(1.。MAXITER)。forEach {
z = z。平方()+ c
如果(ž。squaredModule()> = 4)
归还 它
}
return maxIter
}
您可以在此处看到如何使用运算符重载和数据类来表示复数,这样可以真正简化代码并使其更易于理解。
一旦我们在Complex类中定义了对复数进行操作的规则,该 mandelSet 函数只需要检查操作z < - z ^ 2 + c是否“转义”,并且万一,经过多次迭代后,它将超过4的门槛。
在这里,您可以在AsciiArt中呈现的输出中看到Mandelbrot Set的特征心形图:
背包问题
该背包问题可以通过多种方式来定义。想象一下,作为一个刚刚闯入手表店的小偷。如果您没有超过背包中可携带的最大重量,您可以偷走任意数量的手表。
作为一个实用的小偷,你绝对想要优化你带来的手表的价值。每只手表都有价格和重量。因此,您需要找到具有给定重量的最大总价的手表组。
实际应用包括优化CNC应用的削减和材料以及分配广告预算的营销策略。
例如,让我们看一下只有三只手表的商店,定义如下:
val shop = 背包。商店(
观察(重量 = 1,价格 = 1),
观察(重量 = 3,价格 = 2),
观看(重量 = 1,价格 = 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代码,在memoization函数和最大值的递归搜索中分开。
typealias Memoizer = MutableMap < String,Int >
有趣的 priceAddingElement(备忘录:Memoizer,shop:Set < Watch >,选择:Set < Watch >,maxWeight:Int,priceSum:Int):Int =
商店。过滤 { !(它 在 选择中)&& 它。重量 <= maxWeight }
。地图 {
selectWatches(
备忘录,
商店,
maxWeight - 它。重量,
选择 + 它,
priceSum + 它。价格)}
。过滤 { it > priceSum }
。max()?:priceSum
有趣的 selectWatches(备忘录:Memoizer,shop:Set < Watch >,maxWeight:Int,choice:Set < Watch >,priceSum:Int):Int =
memoization(memo,generateKey(choice)){
priceAddingElement(备忘录,商店,选择,maxWeight,priceSum)}
private fun memoization(memo:Memoizer,key:String,f :()- > Int):Int = when(val w = memo [ key ]){
null - > f()。还 { memo [ key ] = it }
否则 - > w
}
我真的很喜欢Kotlin如何让你清楚地表达意图,而不必重复自己。如果您不了解Kotlin,我希望这段代码可以吸引您,并在某一天尝试它。
基准
现在,您正在等待的部分,让我们比较Graal与优秀的'C2编译器'的性能。
让我们记住,Graal是用Java编写的,并且正在利用编译器领域的新研究,但它仍然相对年轻。另一方面,C2非常好地调整和成熟。
第一个惊喜是Mandelbrot的例子:
说实话,我没想到性能会有这么大的差异。Graal比C2快约18%!只是为了确定,我用稍微不同的公式再次尝试并收到了相同的结果。Graal在编写Kotlin的计算时非常精彩。
而现在,更令人惊讶的是,背包测试:
在这里,Graal慢了54%!
做一些分析,我发现我的代码大部分时间都花在了为memoization生成密钥的函数上。
为了确保我是正确的,我订购了套装,然后将其转换为字符串。这是很多不必要的工作,它依赖于 HashSet Java实现。
所以,我改变了方法来生成密钥:
private fun generateKey(choice:Set < Watch >):String =
选择。sortedBy { “$ {it.price} - $ {it.weight}” }。toString()
对此:
private fun generateKey(choice:Set < Watch >):String =
选择。地图 { 它。hashCode()}。sorted()。joinToString(“”)
新功能更快,因为它对手表的哈希值进行排序,这些哈希值是唯一的,然后将它们连接成一个字符串。
请注意,我们不能简单地使用Set的哈希值,因为可能存在哈希冲突。我实际上尝试过并验证它开始发出错误的结果。
可以为Set创建更安全的散列方法,但这里的目标不是最大限度地优化算法,而是编写高效且清晰的Kotlin代码。
现在,让我们来看看惯用的Kotlin的结果:
在这里,Graal再次明显比C2快,总的来说,新的密钥生成器比以前的实现快得多。
我对这些结果的猜测是,C2经过大量优化(使用内在函数等)用于典型的Java用法,而Graal擅长编译小方法和轻量级对象,这是典型的惯用Kotlin。
我希望这篇文章能激励更多Kotlin开发人员使用Graal!快乐的编码!
©著作权归作者所有:来自51CTO博客作者萤火的萤的原创作品,如需转载,请注明出处,否则将追究法律责任
共同学习,写下你的评论
评论加载中...
作者其他优质文章