-
Notifications
You must be signed in to change notification settings - Fork 6k
KT-77646 - Optimize copyOf(newSize) function for primitive arrays #5455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
@@ -940,7 +940,13 @@ public fun <T> Array<out T>.copyOf(newSize: Int): Array<T?> { | |||
*/ | |||
public actual fun ByteArray.copyOf(newSize: Int): ByteArray { | |||
require(newSize >= 0) { "Invalid new array size: $newSize." } | |||
return fillFrom(this, ByteArray(newSize)) | |||
return if (newSize > this.size) { | |||
val copy = ByteArray(newSize) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT:
ByteArray(newSize).also { copy ->
copy.asDynamic().set(this, 0)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Would a one-liner be ok?
ByteArray(newSize).also { it.asDynamic().set(this, 0) }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer to not use such one liners to keep code more readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Modified to the suggested variant and force-pushed.
42c2833
to
ba9462e
Compare
@lppedd nice job! The new implementation outperforms the old one almost always. The only exception is when the |
@fzhinkin thanks for looking into it. Did you notice the slowdown only for the Admittedly I didn't consider very small arrays, but I guess the only thing we could do here is pick an arbitrary size boundary (e.g. the 32 you mentioned) and add another branch. |
I've run a bunch of new benchmarks https://gist.github.com/lppedd/57d99f5f73acd7f37b96c13f08251361 Basically the TL;DR on my PC is that if the loop is about 20 or less iterations, it will perform slightly better than public actual fun ByteArray.copyOf(newSize: Int): ByteArray {
require(newSize >= 0) { "Invalid new array size: $newSize." }
val size = this.size
return when {
// 20 or whatever other hardcoded limit we want to set
newSize < 20 || size < 20 -> fillFrom(this, ByteArray(newSize))
newSize > size -> ByteArray(newSize).also { copy ->
copy.asDynamic().set(this, 0)
}
else -> this.asDynamic().slice(0, newSize)
}
} Tho I'm not a big fan of complicating functions like this, but I don't have a better suggestion. |
It seems like either of branches could be slower, the main factor here is the number of copied elements. |
@fzhinkin yup I've noticed. I've also updated the code sample in my latest comment about the perf issue to use a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me also (somehow) double check it for browser environment. Perhaps, lower performance for short copies is something NodeJS-specific and should be address by the runtime (thus, we just need to report it to NodeJS).
Thank you! public actual fun ByteArray.copyOf(newSize: Int): ByteArray {
require(newSize >= 0) { "Invalid new array size: $newSize." }
val size = this.size
val limit = min(size, newSize)
return when {
// 20 or whatever other hardcoded limit we want to set
limit < 20 -> fillFrom(this, ByteArray(newSize), limit)
newSize > size -> ByteArray(newSize).also { copy ->
copy.asDynamic().set(this, 0)
}
else -> this.asDynamic().slice(0, newSize)
}
}
private fun fillFrom(src: dynamic, dst: dynamic, limit: Int): dynamic {
var index = 0
val arr = dst.unsafeCast<Array<Any?>>()
while (index < limit) arr[index] = src[index++]
return dst
} But there is close to no difference as far as I can tell, and the |
Double checked results w/ Bun being used instead of NodeJS, and also checked a plain JS code copying arrays in browser (https://jsperf.app/maxije): for short copied, loop is always better :( |
@lppedd, yep, the reworked solution looks good. I think we can stick to relatively small threshold value, say 16. In future, if it ever be a problem, we can always reconsider it. |
All primitive arrays - apart from LongArray and BooleanArray - are compiled to JS TypedArrays. This means that instead of iterating all indexes we can simply use TypedArray.set or TypedArray.slice depending on the inputted new size. However, benchmarking showed that when copying a small number of elements a simple loop is consistently faster than calling TypedArray.set or TypedArray.slice across JS engines. 16 as an arbitrary threshold based on those benchmark results. ^KT-77646
ba9462e
to
5f39969
Compare
Adjusted and force pushed. Thanks! |
See https://youtrack.jetbrains.com/issue/KT-77646
cc @JSMonk