-
Notifications
You must be signed in to change notification settings - Fork 395
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
Conversation
val innerJS = this.innerJS // local copy | ||
val innerWasm = this.innerWasm // local copy |
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.
Do I understand correctly that, these local copies are for better performance (accessing local copy is faster than class field), right?
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.
Yes, that's correct.
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.
[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
.
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.
For ArrayDeque
, it does not seem like we rely all that much on the growability of js.Array
s. 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?
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.
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.
https://colab.research.google.com/drive/17eevbKILkGiT21uqWCDuD3KAkj-de6BT?usp=sharing
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.
Were those benchmarks done in fullLink?
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.
It was in fastLink
, in fullLink
scala.Array and js.Array has a bit smaller difference in addLast bench
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
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 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?
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.
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 👍
I think the difference in poll comes from resizing too, since it push 10000 items (resize happen) and then poll 10000 times.
Perhaps it would be better to split it into two PRs for |
249ba0b
to
1ba7624
Compare
I took a liberty of splitting a PR for PriorityQueue #5168 |
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
1ba7624
to
057d9ad
Compare
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
057d9ad
to
b9c9fcf
Compare
Updated to use |
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.
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 |
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.
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)) |
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.
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)) |
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.
Same here.
fillNulls(endIndex + oldCapacity, inner.length) | ||
val newArr = new Array[AnyRef](oldCapacity * 2) | ||
System.arraycopy(inner, 0, newArr, oldCapacity, endIndex) | ||
inner= newArr |
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:
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) |
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.
This doesn't look right. Shouldn't it be
System.arraycopy(inner, target + 1, inner, target, endIndex - target) | |
System.arraycopy(inner, target + 1, inner, target, endIndex - (target + 1)) |
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.
oops, nice catch, thanks!
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
b9c9fcf
to
08ce2c6
Compare
Thanks, I've addressed all feedbacks (restoring comments I accidentally deleted 🙇) and rewritten the commit message. |
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.
LGTM.
@gzm0 Do you want to also have a look?
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.
https://colab.research.google.com/drive/17eevbKILkGiT21uqWCDuD3KAkj-de6BT?usp=sharing