Skip to content

Optimize CopyOnWriteArrayList to use scala.Array on Wasm #5211

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

Merged
merged 1 commit into from
Jul 29, 2025

Conversation

tanishiking
Copy link
Contributor

This commit optimizes CopyOnWriteArrayList to use a platform-specific internal array (scala.Array on Wasm, and js.Array on JS), similar to what was done for ArrayList.

On WebAssembly, it now uses a scala.Array[AnyRef] for its internal storage. This improves performance by avoiding JS interop overhead for array operations. On JavaScript, it continues to use js.Array. InnerArrayImpl abstracts away the platform-specific details from the main class.

To avoid keeping track the length of array in the main class on Wasm, we store the length of the array at the index 0 of the array.

@tanishiking tanishiking force-pushed the copy-on-write-arraylist branch from e3c20ce to b145cc0 Compare July 21, 2025 10:18
@tanishiking
Copy link
Contributor Author

oops, some tests failed

Copy link
Member

@sjrd sjrd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably port to this PR a number of the latest improvements that were made to PriorityQueue.

protected def innerPush(elem: E): Unit =
inner.push(elem)
protected def innerPush(elem: E): Unit = {
ensureCapacity(size() + 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In CopyOnWriteArrayList, we should keep the regular notion of capacity (without the + 1). It made sense in PriorityQueue because the generic implementation avoids index 0 anyway. But in CopyOnWriteArrayList, avoiding 0 is a Wasm-only thing, due to storing the length.

We should concentrate all those + 1s and - 1s inside JArrayImpl.

inner = innerImpl.resize(inner, minCapacity)
}
}
// We ignore this in JS as js.Array doesn't support explicit pre-allocation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like in PriorityQueue, we can replace the Wasm-only capacity() and resize() by a generic, virtual ensureCapacity() (which happens to do nothing in JSArrayImpl).

}

private[concurrent] sealed abstract class InnerArrayImpl {
type Repr <: AnyRef
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also use Repr[E] here.

@tanishiking tanishiking force-pushed the copy-on-write-arraylist branch from b145cc0 to bb0c77b Compare July 28, 2025 09:21
@tanishiking
Copy link
Contributor Author

tanishiking commented Jul 28, 2025

Now this change follows what PriorityQueue does for InnerArrayImpl.

Also, made a rough benchmark and verified that Wasm performance has improved, while JS remains unchanged
tanishiking/scalajs-benchmarks@7d7c98f

Benchmark main JS HEAD JS main Wasm HEAD Wasm
CopyOnWriteArrayListAdd_100 2.41 ± 0.0025 2.39 ± 0.0025 18.19 ± 0.0111 11.27 ± 0.0083
CopyOnWriteArrayListAdd_1000 32.94 ± 0.0302 32.84 ± 0.0296 185.77 ± 0.1113 102.61 ± 0.0667
CopyOnWriteArrayListGet_100 1.31 ± 0.0007 1.49 ± 0.0007 22.36 ± 0.0035 3.3 ± 0.0008
CopyOnWriteArrayListGet_1000 12.57 ± 0.0025 14.66 ± 0.0022 222.46 ± 0.0646 31.08 ± 0.0068
CopyOnWriteArrayListIterator_100 0.41 ± 0.0001 0.41 ± 0.0004 11.58 ± 0.0037 1.42 ± 0.0017
CopyOnWriteArrayListIterator_1000 4.5 ± 0.001 4.52 ± 0.0014 109.8 ± 0.022 12.19 ± 0.0039
CopyOnWriteArrayListAddRemove_100 23.91 ± 0.0219 24.19 ± 0.0234 76.95 ± 0.0585 17.41 ± 0.0062
CopyOnWriteArrayListAddRemove_1000 238.21 ± 0.4012 235.94 ± 0.224 756.35 ± 0.6057 163.99 ± 0.0378

@tanishiking tanishiking requested a review from sjrd July 28, 2025 14:16
Copy link
Member

@sjrd sjrd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks very good. Only a few nits.

*/

// package private so that `protected def innerSnapshot` can access this field.
private[concurrent] val innerImpl: InnerArrayImpl =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private[concurrent] val innerImpl: InnerArrayImpl =
private[concurrent] val innerImpl: InnerArrayImpl = {

v.splice(index, 0, e.asInstanceOf[AnyRef])
v
}
@inline def add[E](v: Repr[E], index: Int, items: Collection[_ <: E]): Repr[E] = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the methods have significant bodies, in this one, let's have real blank lines between each method, as is our regular coding style.

@inline def add[E](v: Repr[E], index: Int, items: Collection[_ <: E]): Repr[E] = {
val innerIdx = index + 1
val size = length(v)
val newArr = ensureCapacity(v, size + items.size())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are several calls to items.size(). Consider storing that first into a val.

val size = length(v)
val removed = v(innerIdx)
System.arraycopy(v, innerIdx + 1, v, innerIdx, size - innerIdx)
v(size) = null.asInstanceOf[AnyRef] // free reference for GC
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v(size) = null.asInstanceOf[AnyRef] // free reference for GC
v(size) = null // free reference for GC

Comment on lines 425 to 426
private class CopyOnWriteArrayListIterator[E](arraySnapshot: innerImpl.Repr[E],
i: Int, start: Int, end: Int) extends AbstractRandomAccessListIterator[E](i, start, end) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private class CopyOnWriteArrayListIterator[E](arraySnapshot: innerImpl.Repr[E],
i: Int, start: Int, end: Int) extends AbstractRandomAccessListIterator[E](i, start, end) {
private class CopyOnWriteArrayListIterator[E](
arraySnapshot: innerImpl.Repr[E], i: Int, start: Int, end: Int)
extends AbstractRandomAccessListIterator[E](i, start, end) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was it actually necessary to move this inside the companion object? If not, consider leaving it out to keep a smaller diff.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not, moved back to outside of the companion object :)

@tanishiking tanishiking force-pushed the copy-on-write-arraylist branch from bb0c77b to cdc5062 Compare July 29, 2025 05:48
This commit optimizes `CopyOnWriteArrayList` to use a platform-specific
internal array (scala.Array on Wasm, and js.Array on JS),
similar to what was done for `ArrayList`.

On WebAssembly, it now uses a `scala.Array[AnyRef]` for its internal
storage. This improves performance by avoiding JS interop overhead for
array operations. On JavaScript, it continues to use `js.Array`.
InnerArrayImpl abstracts away the platform-specific details from the main class.

To avoid keeping track the length of array in the main class on Wasm,
we store the length of the array at the index 0 of the array.
@tanishiking tanishiking force-pushed the copy-on-write-arraylist branch from cdc5062 to 718d3ec Compare July 29, 2025 05:49
@sjrd sjrd enabled auto-merge July 29, 2025 06:27
@sjrd sjrd merged commit d6d6a37 into scala-js:main Jul 29, 2025
3 checks passed
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.

2 participants