Skip to content

Implement ArrayDeque using scala.Array. #5164

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
May 26, 2025

Conversation

tanishiking
Copy link
Contributor

@tanishiking tanishiking commented May 9, 2025

Simliar to ju.ArrayList #5162, add a Wasm version of implementation of ArrayDeque that doesn't have JS-interop for internal data structure.


I wrote a micro-benchmark for ArrayDeque, and compare the two implementation on JS (scala.Array vs js.Array), each benchmark basically add/poll integer 10000 times.
tanishiking/scalajs-benchmarks@13cd090

In WebAssembly, it works around 6~7x- faster than js.Array impl.

download (3)

https://colab.research.google.com/drive/17eevbKILkGiT21uqWCDuD3KAkj-de6BT?usp=sharing

Comment on lines 369 to 370
val innerJS = this.innerJS // local copy
val innerWasm = this.innerWasm // local copy
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do I understand correctly that, these local copies are for better performance (accessing local copy is faster than class field), right?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, that's correct.

Copy link
Contributor Author

@tanishiking tanishiking May 15, 2025

Choose a reason for hiding this comment

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

[note]
This is also a problem, let's wait until we decide to use (or not to use) Array[AnyRef] anyway.
If we don't, we can do something like val innerJS = if (isWebAssembly) null else this.innerJS, or maybe we can do the same as PriorityQueue.

@tanishiking tanishiking changed the title Wasm: Implement ArrayDeque without js.Array in Wasm backend Wasm: Implement ArrayDeque and PriorityQueue without js.Array in Wasm backend May 9, 2025
Copy link
Member

Choose a reason for hiding this comment

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

For ArrayDeque, it does not seem like we rely all that much on the growability of js.Arrays. The only place that grows is ensureCapacityForAdd(). It always doubles the size of the internal array. It's unlikely that the underlying VM could do a better job than just reallocating at about the same moments.

Perhaps it would be better to just always use Array[AnyRef], even on JS. That would avoid complicating the logic, which in this class is particularly dense already.

@gzm0 WDYT?

Copy link
Contributor Author

@tanishiking tanishiking May 14, 2025

Choose a reason for hiding this comment

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

For comparison, I wrote a Array[AnyRef] (even on JS) version b73e8a1

And, I wrote a micro-benchmark for ArrayDeque, and compare the two implementation on JS (scala.Array vs js.Array), each benchmark basically add/poll integer 10000 times.
tanishiking/scalajs-benchmarks@13cd090

The result shows that, js.Array is still slightly faster than scala.Array in most cases. However, considering the complexity of the program, the difference may be negligible.

download

https://colab.research.google.com/drive/17eevbKILkGiT21uqWCDuD3KAkj-de6BT?usp=sharing

Copy link
Member

Choose a reason for hiding this comment

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

Were those benchmarks done in fullLink?

Copy link
Contributor Author

@tanishiking tanishiking May 15, 2025

Choose a reason for hiding this comment

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

It was in fastLink, in fullLink scala.Array and js.Array has a bit smaller difference in addLast bench

download (2)

https://colab.research.google.com/drive/17eevbKILkGiT21uqWCDuD3KAkj-de6BT?usp=sharing

it's interesting that, with scala.Array impl, FIFO and LIFO cases (last two benchmarks) are consistently faster than js.Array

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with the overall sentiment, that just using scala.Array for JS as well might be worthwhile.

It seems that the investigation here as already gone beyond gut feeling, so I guess we can let the numbers speak.

The current benchmarks do look like there is some overhead not related to resizing. For example, I'd expect peekFirst_Repeatedly to not cause any mutations. Do we have an understanding of where these come from?

Copy link
Contributor Author

@tanishiking tanishiking May 16, 2025

Choose a reason for hiding this comment

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

Ah, it was my benchmark programs' fault, the peekFist/peekLast benchmarks anyway add 10000 items to the queue, and resize happened.

Updated the benchmark to add only 16 items to the queue (not to trigger resizing) and peek first/last 10000 times. tanishiking/scalajs-benchmarks@825063f

The result shows that there's really no difference between js.Array and scala.Array 👍

download (4)

I think the difference in poll comes from resizing too, since it push 10000 items (resize happen) and then poll 10000 times.

@tanishiking
Copy link
Contributor Author

Perhaps it would be better to split it into two PRs for ArrayDeque and PriorityQueue?

@tanishiking tanishiking changed the title Wasm: Implement ArrayDeque and PriorityQueue without js.Array in Wasm backend Wasm: Implement ArrayDeque without js.Array in Wasm backend May 15, 2025
@tanishiking
Copy link
Contributor Author

I took a liberty of splitting a PR for PriorityQueue #5168
Let's focus on ArrayDeque here.

tanishiking added a commit to tanishiking/scala-js that referenced this pull request May 21, 2025
The original implementation of `ju.ArrayDeque` used `js.Array` for its internal data structure.
When compiling to WebAssembly, it requires JavaScript interop calls to access the underlying js.Array, leading to a significant performance overhead.

This commit optimize the `ju.ArrayDeque` for WebAssembly backend by switching the `ju.ArrayDeque` implementation to use `scala.Array` instead.
We've opted for `scala.Array` consistently across both WebAssembly and JavaScript environments, rather than conditionally switching between `js.Array` and `scala.Array`, to avoid complicate the logic by inserting a bunch of if/else here and there.
Also, there's minor performance degradation observed when using scala.Array and managing size in user-space on JavaScript is considered acceptable.

See the discussion at scala-js#5164
tanishiking added a commit to tanishiking/scala-js that referenced this pull request May 21, 2025
The original implementation of `ju.ArrayDeque` used `js.Array` for its internal data structure.
When compiling to WebAssembly, it requires JavaScript interop calls to access the underlying js.Array, leading to a significant performance overhead.

This commit optimize the `ju.ArrayDeque` for WebAssembly backend by switching the `ju.ArrayDeque` implementation to use `scala.Array` instead.
We've opted for `scala.Array` consistently across both WebAssembly and JavaScript environments, rather than conditionally switching between `js.Array` and `scala.Array`, to avoid complicate the logic by inserting a bunch of if/else here and there.
Also, there's minor performance degradation observed when using scala.Array and managing size in user-space on JavaScript is considered acceptable.

See the discussion at scala-js#5164
@tanishiking
Copy link
Contributor Author

Updated to use scala.Array anyway on JS and WebAssembly based on the conversation here #5164 (comment)

@tanishiking tanishiking requested review from sjrd and gzm0 May 25, 2025 20:23
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.

Oops, sorry for the delay.

@@ -18,14 +18,13 @@ import java.lang.Utils._
import java.util.ScalaOps._

import scala.scalajs.js
import scala.scalajs.LinkingInfo
Copy link
Member

Choose a reason for hiding this comment

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

This import is not used anymore.

And I guess the import scala.scalajs.js isn't used anymore either.

@@ -327,7 +326,7 @@ class ArrayDeque[E] private (initialCapacity: Int)
do {
if (i >= capacity)
i = 0
if (Objects.equals(inner(i), o))
if (Objects.equals(inner(i).asInstanceOf[E], o))
Copy link
Member

Choose a reason for hiding this comment

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

The cast is probably not necessary here. Object.equals accepts any type of argument.

@@ -346,7 +345,7 @@ class ArrayDeque[E] private (initialCapacity: Int)
i -= 1
if (i < 0)
i = inner.length - 1
if (Objects.equals(inner(i), o))
if (Objects.equals(inner(i).asInstanceOf[E], o))
Copy link
Member

Choose a reason for hiding this comment

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

Same here.

fillNulls(endIndex + oldCapacity, inner.length)
val newArr = new Array[AnyRef](oldCapacity * 2)
System.arraycopy(inner, 0, newArr, oldCapacity, endIndex)
inner= newArr
Copy link
Member

Choose a reason for hiding this comment

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

Nit:

Suggested change
inner= newArr
inner = newArr

for (i <- target until endIndex - 1)
inner(i) = inner(i + 1)
inner(endIndex - 1) = null.asInstanceOf[E] // free reference for GC
System.arraycopy(inner, target + 1, inner, target, endIndex - target)
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't look right. Shouldn't it be

Suggested change
System.arraycopy(inner, target + 1, inner, target, endIndex - target)
System.arraycopy(inner, target + 1, inner, target, endIndex - (target + 1))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oops, nice catch, thanks!

@sjrd
Copy link
Member

sjrd commented May 25, 2025

Oh also, the commit message needs to be changed. It's not Wasm-only anymore.

The original implementation of `ju.ArrayDeque` used `js.Array` for its internal data structure.
However, when compiling to WebAssembly, it requires JavaScript interop calls are required to access the underlying js.Array, leading to a significant performance overhead.

This commit switches the internal data structure of ju.ArrayDeque to use scala.Array instead, for both the WebAssembly and JavaScript backends.
Using `scala.Array` in both environments avoids complicating the logic by conditionally using `js.Array` or `scala.Array` based on whether it's Wasm or JS.

This change significantly improves ArrayDeque's performance on WebAssembly and results in only minor performance degradation on JavaScript.

See the discussion at scala-js#5164
@tanishiking
Copy link
Contributor Author

Thanks, I've addressed all feedbacks (restoring comments I accidentally deleted 🙇) and rewritten the commit message.

@tanishiking tanishiking requested a review from sjrd May 26, 2025 05:11
@sjrd sjrd changed the title Wasm: Implement ArrayDeque without js.Array in Wasm backend Implement ArrayDeque using scala.Array. May 26, 2025
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.

LGTM.

@gzm0 Do you want to also have a look?

@gzm0 gzm0 merged commit e293a4c into scala-js:main May 26, 2025
3 checks passed
@tanishiking tanishiking deleted the arraydeque-wasm branch May 27, 2025 02:34
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.

5 participants