Skip to content

Conversation

lppedd
Copy link
Contributor

@lppedd lppedd commented May 30, 2025

@lppedd lppedd requested review from ilya-g and fzhinkin as code owners May 30, 2025 17:19
@fzhinkin fzhinkin requested a review from JSMonk May 30, 2025 19:03
@@ -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)
Copy link
Member

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)
}

Copy link
Contributor Author

@lppedd lppedd Jun 1, 2025

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) }

Copy link
Member

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.

Copy link
Contributor Author

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.

@lppedd lppedd force-pushed the feat/js-typedarrays branch from 42c2833 to ba9462e Compare June 2, 2025 09:41
JSMonk
JSMonk previously approved these changes Jun 2, 2025
@fzhinkin
Copy link
Contributor

fzhinkin commented Jun 2, 2025

@lppedd nice job! The new implementation outperforms the old one almost always. The only exception is when the newSize is relatively small (empirically, < 32), when the old implementation is significantly faster. Could you please check if something could be done for shorter copies?

@lppedd
Copy link
Contributor Author

lppedd commented Jun 2, 2025

@fzhinkin thanks for looking into it. Did you notice the slowdown only for the set branch, or for both set and slice?

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.

@lppedd
Copy link
Contributor Author

lppedd commented Jun 2, 2025

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 slice or set.
So what we might want to do is something like

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.

@fzhinkin
Copy link
Contributor

fzhinkin commented Jun 2, 2025

@lppedd

Did you notice the slowdown only for the set branch, or for both set and slice?

It seems like either of branches could be slower, the main factor here is the number of copied elements.

@lppedd
Copy link
Contributor Author

lppedd commented Jun 2, 2025

@fzhinkin yup I've noticed. I've also updated the code sample in my latest comment about the perf issue to use a when expression. But yeah I don't have other ideas.

Copy link
Contributor

@fzhinkin fzhinkin left a 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).

@lppedd
Copy link
Contributor Author

lppedd commented Jun 2, 2025

Thank you!
In the meantime I've experimented with a slightly modified version to reduce the conditions to evaluate by using Math.min.

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 Math.min version might be worse with 20+ elements.

@fzhinkin
Copy link
Contributor

fzhinkin commented Jun 4, 2025

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
Copy link
Contributor Author

lppedd commented Jun 4, 2025

@fzhinkin damn, well at least the behavior is consistent between implementations!

Are we ok in going ahead with the reworked solution?
Also worth adding a comment to explain why the additional branch is there.

@fzhinkin
Copy link
Contributor

fzhinkin commented Jun 4, 2025

@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
@lppedd
Copy link
Contributor Author

lppedd commented Jun 5, 2025

I think we can stick to relatively small threshold value, say 16

Adjusted and force pushed. Thanks!

@lppedd lppedd requested a review from JSMonk June 5, 2025 17:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants