Skip to content

Commit 5f39969

Browse files
committed
[K/JS] Optimize copyOf(newSize) function for primitive arrays
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
1 parent 7f600d7 commit 5f39969

File tree

2 files changed

+84
-14
lines changed

2 files changed

+84
-14
lines changed

libraries/stdlib/js/src/generated/_ArraysJs.kt

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,14 @@ public fun <T> Array<out T>.copyOf(newSize: Int): Array<T?> {
940940
*/
941941
public actual fun ByteArray.copyOf(newSize: Int): ByteArray {
942942
require(newSize >= 0) { "Invalid new array size: $newSize." }
943-
return fillFrom(this, ByteArray(newSize))
943+
val size = this.size
944+
return when {
945+
newSize < 16 || size < 16 -> fillFrom(this, ByteArray(newSize))
946+
newSize > size -> ByteArray(newSize).also { copy ->
947+
copy.asDynamic().set(this, 0)
948+
}
949+
else -> this.asDynamic().slice(0, newSize)
950+
}
944951
}
945952

946953
/**
@@ -954,7 +961,14 @@ public actual fun ByteArray.copyOf(newSize: Int): ByteArray {
954961
*/
955962
public actual fun ShortArray.copyOf(newSize: Int): ShortArray {
956963
require(newSize >= 0) { "Invalid new array size: $newSize." }
957-
return fillFrom(this, ShortArray(newSize))
964+
val size = this.size
965+
return when {
966+
newSize < 16 || size < 16 -> fillFrom(this, ShortArray(newSize))
967+
newSize > size -> ShortArray(newSize).also { copy ->
968+
copy.asDynamic().set(this, 0)
969+
}
970+
else -> this.asDynamic().slice(0, newSize)
971+
}
958972
}
959973

960974
/**
@@ -968,7 +982,14 @@ public actual fun ShortArray.copyOf(newSize: Int): ShortArray {
968982
*/
969983
public actual fun IntArray.copyOf(newSize: Int): IntArray {
970984
require(newSize >= 0) { "Invalid new array size: $newSize." }
971-
return fillFrom(this, IntArray(newSize))
985+
val size = this.size
986+
return when {
987+
newSize < 16 || size < 16 -> fillFrom(this, IntArray(newSize))
988+
newSize > size -> IntArray(newSize).also { copy ->
989+
copy.asDynamic().set(this, 0)
990+
}
991+
else -> this.asDynamic().slice(0, newSize)
992+
}
972993
}
973994

974995
/**
@@ -996,7 +1017,14 @@ public actual fun LongArray.copyOf(newSize: Int): LongArray {
9961017
*/
9971018
public actual fun FloatArray.copyOf(newSize: Int): FloatArray {
9981019
require(newSize >= 0) { "Invalid new array size: $newSize." }
999-
return fillFrom(this, FloatArray(newSize))
1020+
val size = this.size
1021+
return when {
1022+
newSize < 16 || size < 16 -> fillFrom(this, FloatArray(newSize))
1023+
newSize > size -> FloatArray(newSize).also { copy ->
1024+
copy.asDynamic().set(this, 0)
1025+
}
1026+
else -> this.asDynamic().slice(0, newSize)
1027+
}
10001028
}
10011029

10021030
/**
@@ -1010,7 +1038,14 @@ public actual fun FloatArray.copyOf(newSize: Int): FloatArray {
10101038
*/
10111039
public actual fun DoubleArray.copyOf(newSize: Int): DoubleArray {
10121040
require(newSize >= 0) { "Invalid new array size: $newSize." }
1013-
return fillFrom(this, DoubleArray(newSize))
1041+
val size = this.size
1042+
return when {
1043+
newSize < 16 || size < 16 -> fillFrom(this, DoubleArray(newSize))
1044+
newSize > size -> DoubleArray(newSize).also { copy ->
1045+
copy.asDynamic().set(this, 0)
1046+
}
1047+
else -> this.asDynamic().slice(0, newSize)
1048+
}
10141049
}
10151050

10161051
/**
@@ -1038,7 +1073,15 @@ public actual fun BooleanArray.copyOf(newSize: Int): BooleanArray {
10381073
*/
10391074
public actual fun CharArray.copyOf(newSize: Int): CharArray {
10401075
require(newSize >= 0) { "Invalid new array size: $newSize." }
1041-
return withType("CharArray", fillFrom(this, CharArray(newSize)))
1076+
val size = this.size
1077+
val copy = when {
1078+
newSize < 16 || size < 16 -> fillFrom(this, CharArray(newSize))
1079+
newSize > size -> CharArray(newSize).also { copy ->
1080+
copy.asDynamic().set(this, 0)
1081+
}
1082+
else -> this.asDynamic().slice(0, newSize)
1083+
}
1084+
return withType("CharArray", copy)
10421085
}
10431086

10441087
/**

libraries/tools/kotlin-stdlib-gen/src/templates/Arrays.kt

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,14 +1076,41 @@ object ArrayOps : TemplateGroupBase() {
10761076
returns("SELF")
10771077
on(Platform.JS) {
10781078
when (primitive!!) {
1079-
PrimitiveType.Boolean ->
1080-
body { "return withType(\"BooleanArray\", arrayCopyResize(this, newSize, false))" }
1081-
PrimitiveType.Char ->
1082-
body { "return withType(\"CharArray\", fillFrom(this, ${primitive}Array(newSize)))" }
1083-
PrimitiveType.Long ->
1084-
body { "return withType(\"LongArray\", arrayCopyResize(this, newSize, ${primitive!!.zero()}))" }
1085-
else ->
1086-
body { "return fillFrom(this, ${primitive}Array(newSize))" }
1079+
PrimitiveType.Long -> body {
1080+
"return withType(\"LongArray\", arrayCopyResize(this, newSize, ${primitive!!.zero()}))"
1081+
}
1082+
PrimitiveType.Boolean -> body {
1083+
"return withType(\"BooleanArray\", arrayCopyResize(this, newSize, false))"
1084+
}
1085+
// Benchmarking showed that when copying a small number of elements (typically < 20),
1086+
// a simple loop is consistently faster than calling TypedArray.set or TypedArray.slice
1087+
// across JS engines. We chose 16 as an arbitrary threshold based on those results.
1088+
PrimitiveType.Char -> body {
1089+
"""
1090+
val size = this.size
1091+
val copy = when {
1092+
newSize < 16 || size < 16 -> fillFrom(this, CharArray(newSize))
1093+
newSize > size -> CharArray(newSize).also { copy ->
1094+
copy.asDynamic().set(this, 0)
1095+
}
1096+
else -> this.asDynamic().slice(0, newSize)
1097+
}
1098+
1099+
return withType("CharArray", copy)
1100+
""".trimIndent()
1101+
}
1102+
else -> body {
1103+
"""
1104+
val size = this.size
1105+
return when {
1106+
newSize < 16 || size < 16 -> fillFrom(this, ${primitive}Array(newSize))
1107+
newSize > size -> ${primitive}Array(newSize).also { copy ->
1108+
copy.asDynamic().set(this, 0)
1109+
}
1110+
else -> this.asDynamic().slice(0, newSize)
1111+
}
1112+
""".trimIndent()
1113+
}
10871114
}
10881115
body { newSizeCheck + "\n" + body }
10891116
}

0 commit comments

Comments
 (0)