lastIndex vs (size -1) in kotlin

huangapple go评论50阅读模式
英文:

lastIndex vs (size -1) in kotlin

问题

为什么在intArray中使用lastIndex比使用size - 1要慢?lastIndex方法的实现使用get() = size - 1,但为什么要花费这么长时间?

fun main() {
    val tar = 2
    var nums = intArrayOf(0, 1, 2, 2, 3, 0, 4, 2)
    var count = 0
    var i = 0

    var begin = System.nanoTime()
    var n = nums.size - 1
    while (i <= n) {
        if (nums[i] != tar) {
            nums[count++] = nums[i]
        }
        i++
    }

    var end = System.nanoTime()
    println("${end - begin}")
    nums = intArrayOf(0, 1, 2, 2, 3, 0, 4, 2)
    count = 0
    i = 0

    begin = System.nanoTime()
    n = nums.lastIndex
    while (i <= n) {
        if (nums[i] != tar) {
            nums[count++] = nums[i]
        }
        i++
    }

    end = System.nanoTime()
    println("${end - begin}")
}

我尝试过搜索,但没有找到答案。

英文:

Why is using lastIndex in intArray slower than using size - 1? The implementation of the lastIndex method uses get() = size - 1, but then why does it take so long?

fun main() {
    val tar = 2
    var nums = intArrayOf(0, 1, 2, 2, 3, 0, 4, 2)
    var count = 0
    var i = 0

    var begin = System.nanoTime()
    var n = nums.size - 1
    while (i &lt;= n) {
        if (nums[i] != tar) {
            nums[count++] = nums[i]
        }
        i++
    }

    var end = System.nanoTime()
    println(&quot;${end - begin}&quot;)
    nums = intArrayOf(0, 1, 2, 2, 3, 0, 4, 2)
    count = 0
    i = 0

    begin = System.nanoTime()
    n = nums.lastIndex
    while (i &lt;= n) {
        if (nums[i] != tar) {
            nums[count++] = nums[i]
        }
        i++
    }

    end = System.nanoTime()
    println(&quot;${end - begin}&quot;)
}

I tried googling and found nothing

答案1

得分: 2

正如许多人在评论中已经提到的,通过编写这样简单的基准测试来衡量JVM的性能几乎没有意义。正确的基准测试需要多次运行代码,进行热身,分叉,确保JIT优化正在使用,但与此同时这些优化不会删除我们的代码,等等。基准测试通常需要一些知识和技能。最好使用像JMH这样的工具,以减少我们做错事的风险。

在你的特定情况下,我认为差异甚至不是由于nums.size - 1nums.lastIndex之间的区别。真正的原因是:当我们到达lastIndex行时,它必须加载包含此函数的ArraysKt类。实际上,你测量的是类加载过程,而不是lastIndex的执行时间。在测量的行之前再添加另一个lastIndex,即使是不同的对象,性能也会立即改善。

你的情况是一个很好的例子,说明我们不应该编写这样的基准测试。我们可能会意外地测量到与我们计划的完全不同的东西。

英文:

As many people already said in comments, measuring the performance of JVM by writing such simple benchmarks is pretty much meaningless. Proper benchmarking requires running the code multiple times, doing a warmup, forking, making sure JIT optimizations are in use, but at the same time these optimizations don't remove our code, etc. Benchmarking generally requires some knowledge and skills. It is better to use tools like JMH which decrease the risk we do something wrong.

In your specific case I believe the difference is not even due to nums.size - 1 vs nums.lastIndex. The real reason is: when we get to the lastIndex line, it has to load the ArraysKt class where this function is located. You actually measured the class loading process, not lastIndex execution time. Add another lastIndex anywhere before the measured line, even for a different object, and the performance immediately improves.

Your case is a good example why we shouldn't write such benchmarks. We may accidentally measure something entirely different than we planned.

答案2

得分: 0

的确,使用 lastIndex 较慢,但数量级相当惊人 🤔。那么,让我们深入研究一下 lastIndex vs (size -1) in kotlin

我稍微修改了您的测试,将“关键部分”移到函数中,并在测试过程中不使用IO、变量重新分配、分配等:

// 实际上,我们应该从这些方法中移除时间计算
// 因为在我们执行的测试中,它们不重要
//
// 顺便说一下,Kotlin支持在其标准库中测量时间 :)
// https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.system/measure-time-millis.html 

fun bySizeMinusOne(): Long {
    val tar = 2
    val nums = intArrayOf(0, 1, 2, 2, 3, 0, 4, 2)
    var count = 0
    var i = 0

    val begin = System.nanoTime()
    val n = nums.size - 1
    while (i <= n) {
        if (nums[i] != tar) {
            nums[count++] = nums[i]
        }
        i++
    }

    val end = System.nanoTime()
    return end - begin
}

fun byLastIndex(): Long {
    val tar = 2
    val nums = intArrayOf(0, 1, 2, 2, 3, 0, 4, 2)
    var count = 0
    var i = 0

    val begin = System.nanoTime()
    val n = nums.lastIndex
    // val n = nums.size - 1
    while (i <= n) {
        if (nums[i] != tar) {
            nums[count++] = nums[i]
        }
        i++
    }

    val end = System.nanoTime()
    return end - begin
}

所以,让我们运行我们的测试:

fun main() {
    val bySize = bySizeMinusOne()
    val byLastIndex = byLastIndex()

    println(bySize)
    println(byLastIndex)
}

打印结果如下:

1090
22942410

所以,找出发生了什么的下一步是反编译我们的代码。
唯一的区别是

> size minus one
L5
 LINENUMBER 16 L5
 ALOAD 1
 ARRAYLENGTH
 ICONST_1
 ISUB
 ISTORE 6

> last index
L5	
 LINENUMBER 35 L5	
 ALOAD 1	
 INVOKESTATIC kotlin/collections/ArraysKt.getLastIndex ([I)I	
 ISTORE 6	

于是我们就明白了。调用另一个类并不那么简单,因为JVM需要加载该类。

所以,让我们通过预加载我们需要的方法/类来“改进”我们的测试:

fun main() {

    // 预加载
    val array = intArrayOf() // 加载类
    array.size
    array.lastIndex

    val byLastIndex = byLastIndex()
    val bySize = bySizeMinusOne()

    println(bySize)
    println(byLastIndex)
}

然后我们得到了类似以下的结果:

420
1620

因此,预期会有一些开销(我们可以在字节码中看到差异)。

还有另一个方面 - 在编译生产就绪的JAR文件时,可以应用一些优化。每当测试这些事情时,我们需要记住将关键部分移动到不包括我们在这里提到的内容(IO、内存分配等)的函数中。我们不应该依赖一个结果 - 我们应该多次运行测试,并检查平均值、最大/最小值、百分位数等等。

英文:

Indeed, using lastIndex is slower, but the order of magnitude is quite surprising 🤔.
So, let's deep dive into it lastIndex vs (size -1) in kotlin

I slightly change your test, to move a "critical sections" to functions, and not use IO, variables reassignment, allocation, etc. while testing:

// actually, we should remove time-calculation from those methods
// as they are not important in the test we perform here
// 
// btw. kotlin supports time measurment in its standard library :)
// https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.system/measure-time-millis.html 

fun bySizeMinusOne(): Long {
    val tar = 2
    val nums = intArrayOf(0, 1, 2, 2, 3, 0, 4, 2)
    var count = 0
    var i = 0

    val begin = System.nanoTime()
    val n = nums.size - 1
    while (i &lt;= n) {
        if (nums[i] != tar) {
            nums[count++] = nums[i]
        }
        i++
    }

    val end = System.nanoTime()
    return end - begin
}

fun byLastIndex(): Long {
    val tar = 2
    val nums = intArrayOf(0, 1, 2, 2, 3, 0, 4, 2)
    var count = 0
    var i = 0

    val begin = System.nanoTime()
    val n = nums.lastIndex
    // val n = nums.size - 1
    while (i &lt;= n) {
        if (nums[i] != tar) {
            nums[count++] = nums[i]
        }
        i++
    }

    val end = System.nanoTime()
    return end - begin
}

So, let's run our tests:

fun main() {
    val bySize = bySizeMinusOne()
    val byLastIndex = byLastIndex()

    println(bySize)
    println(byLastIndex)
}

Is printing:

1090
22942410

So, a next step to find out what's going on is to decompile our code.
The only difference is

&gt; size minus one
L5
 LINENUMBER 16 L5
 ALOAD 1
 ARRAYLENGTH
 ICONST_1
 ISUB
 ISTORE 6

&gt; last index
L5	
 LINENUMBER 35 L5	
 ALOAD 1	
 INVOKESTATIC kotlin/collections/ArraysKt.getLastIndex ([I)I	
 ISTORE 6	

And here we are. It's not so simple to call another class, as JVM needs to load the class.

So, let's "improve" our testing by pre-loading methods/classes we need:

fun main() {

    // preload
    val array = intArrayOf() // load class
    array.size
    array.lastIndex

    val byLastIndex = byLastIndex()
    val bySize = bySizeMinusOne()

    println(bySize)
    println(byLastIndex)
}

Then we get something like:

420
1620

So the overhead is expected (we can see the difference in the bytecode).

There is another aspect - when compiling a production-ready jar, there can be a number of optimizations applied. Whenever testing such things, we need to remember to move the critical section to a function which is not including things we have mentioned here (IO, memory allocation etc.). And we should not rely on one result - we should run tests multiple time and check things like average, max/min, percentiles etc.

huangapple
  • 本文由 发表于 2023年7月10日 23:46:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/76655352.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定