首先,让我们谈谈测试方法:

所有 测试 都 在 我的 笔记本 上 运行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  暂停。所有的 测试 均 运行 单- 线程。

 

以下是结果:

JVM Advent Calendar:将Kotlin性能与Graal和C2进行比较

正如你所看到的,Graal使用Kotlin进行编译在小板上的速度明显更快,并且随着大板随机播放的速度稍快一点。

我怀疑这是因为随着更大的主板内存管理,它占用了大量的运行时间。在任何情况下,增量都是受欢迎的,特别是考虑到我通常在较小的板上玩。

为了测试我的理论,我创建了一个新项目,其中包含一些经典算法的实现,以查看性能的差异。你可以在这里找到它。

目前,有两种算法:Mandelbrot Set生成器和Knapsack解算器。

Mandelbrot集

Mandelbrot集可能是你见过的最有名的分形-即使你不知道这个名字。

在数学上,它被定义为复平面中所有点的集合,其中函数z < - z ^ 2 + c在迭代时不会发散。生成Set迭代复杂平面上某些点的函数并从中创建图像非常容易。

由于这里的目标是性能而不是图形,我使用文本图形保持简单。

让我们从查看Mandelbrot Set的代码开始。

数据  Complexval  rDoubleval  iDouble){
    操作员 欢乐 时光(其他:复杂)=
        复杂(
            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的特征心形图:

JVM Advent Calendar:将Kotlin性能与Graal和C2进行比较

背包问题

背包问题可以通过多种方式来定义。想象一下,作为一个刚刚闯入手表店的小偷。如果您没有超过背包中可携带的最大重量,您可以偷走任意数量的手表。

作为一个实用的小偷,你绝对想要优化你带来的手表的价值。每只手表都有价格和重量。因此,您需要找到具有给定重量的最大总价的手表组。

实际应用包括优化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  =  whenval  w  =  memo [ key ]){
        null  - >  f()。还 { memo [ key ] =  it }
        否则 - >  w
    }

 

我真的很喜欢Kotlin如何让你清楚地表达意图,而不必重复自己。如果您不了解Kotlin,我希望这段代码可以吸引您,并在某一天尝试它。

基准

现在,您正在等待的部分,让我们比较Graal与优秀的'C2编译器'的性能。

让我们记住,Graal是用Java编写的,并且正在利用编译器领域的新研究,但它仍然相对年轻。另一方面,C2非常好地调整和成熟。

第一个惊喜是Mandelbrot的例子:

JVM Advent Calendar:将Kotlin性能与Graal和C2进行比较

说实话,我没想到性能会有这么大的差异。Graal比C2快约18%!只是为了确定,我用稍微不同的公式再次尝试并收到了相同的结果。Graal在编写Kotlin的计算时非常精彩。

而现在,更令人惊讶的是,背包测试:JVM Advent Calendar:将Kotlin性能与Graal和C2进行比较

在这里,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的结果:

JVM Advent Calendar:将Kotlin性能与Graal和C2进行比较

在这里,Graal再次明显比C2快,总的来说,新的**生成器比以前的实现快得多。

我对这些结果的猜测是,C2经过大量优化(使用内在函数等)用于典型的Java用法,而Graal擅长编译小方法和轻量级对象,这是典型的惯用Kotlin。

 我希望这篇文章能激励更多Kotlin开发人员使用Graal!快乐的编码!

相关文章: