-
Notifications
You must be signed in to change notification settings - Fork 396
Wasm: Implement PriorityQueue without js.Array in Wasm backend #5168
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
base: main
Are you sure you want to change the base?
Conversation
|
||
// The index 0 is not used; the root is at index 1. | ||
// This is standard practice in binary heaps, to simplify arithmetics. | ||
private var _size = 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.
Something went wrong during the refactoring, because this state variable was moved to the global impl object. 😅
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, thanks, moved back to the PriorityQueue
6f8093e
to
ea2171f
Compare
ea2171f
to
bca2c21
Compare
bca2c21
to
da5c453
Compare
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.
Partial review. We need to address the _size
issue before going further.
|
||
private sealed abstract class InnerArrayImpl { |
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 should be in the companion object of PriorityQueue
, so that instances do not depend on the enclosing PriorityQueue
object.
private sealed abstract class InnerArrayImpl { | |
} | |
object PriorityQueue { | |
private sealed abstract class InnerArrayImpl { |
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! I now understand that they actually refer to the enclosing object for _size
. 😱 That's super confusing: the dependencies on inner
are made explicit (params), but those on _size
are implicit (scope).
We should also pass _size
explicitly. But then that poses an issue for methods that need to return a new Repr
and a new size
. 😖
Here is a crazy idea: we have an unused slot at index 0 of the array. What if we use it to store the size
on Wasm? It would be boxed, but unless it exceeds 2^30, it will fit in a (ref i31)
, so no JS interop should occur. (and if it does exceed 2^30, I don't think that's going to be the bottleneck :p).
This way we still have a single Repr
that embodies both the size and the capacity, on Wasm as well as on JS.
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.
Thanks! I think it's reasonable technique to re-use the free space to store size
information 👍
Here I make a prototype implementation for moving InnerArrayImpl
to companion object, and store size to array(0)
0832ba7
* the function calls. | ||
*/ | ||
|
||
private val innerImpl: InnerArrayImpl = |
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 is unfortunate to store this field inside instances of PriorityQueue
. I think this can be avoided by putting this private val
in the companion object of PriorityQueue
.
If my prediction is correct, that companion will entirely disappear after optimizations.
import java.lang.Utils.roundUpToPowerOfTwo | ||
|
||
import scala.annotation.tailrec |
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.
Imports of scala.annotation.*
always come first (after scala.language.*
imports, if any):
import java.lang.Utils.roundUpToPowerOfTwo | |
import scala.annotation.tailrec | |
import scala.annotation.tailrec | |
import java.lang.Utils.roundUpToPowerOfTwo |
// The index 0 is not used; the root is at index 1. | ||
// This is standard practice in binary heaps, to simplify arithmetics. |
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.
We should keep these two lines as comments on private var inner
. They apply to JS as well.
Also, that means that the notion of "capacity" is a skewed in this class. The array should have 1 more elements than the capacity, for "capacity" being the external notion of capacity.
Put differently, it is probably easier to think of "capacity" as the internal notion of capacity, which is 1 more than the "public capacity". The public capacity only shows up in def this(initialCapacity)
and def this(c: Collection)
.
{ | ||
if (initialCapacity < 1) | ||
throw new IllegalArgumentException | ||
initialCapacity |
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.
initialCapacity | |
initialCapacity + 1 // index 0 is unused |
@@ -47,47 +62,74 @@ class PriorityQueue[E] private ( | |||
NaturalComparator.select(c.comparator().asInstanceOf[Comparator[_ >: E]]) | |||
case _ => | |||
NaturalComparator | |||
}, internal = true) | |||
}, internal = true, roundUpToPowerOfTwo(c.size())) |
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.
}, internal = true, roundUpToPowerOfTwo(c.size())) | |
}, internal = true, roundUpToPowerOfTwo(c.size() + 1)) // index 0 is unused |
addAll(c) | ||
} | ||
|
||
def this(c: PriorityQueue[_ <: E]) = { | ||
this(c.comp.asInstanceOf[Comparator[_ >: E]], internal = true) | ||
this(c.comp.asInstanceOf[Comparator[_ >: E]], internal = true, roundUpToPowerOfTwo(c.size())) |
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(c.comp.asInstanceOf[Comparator[_ >: E]], internal = true, roundUpToPowerOfTwo(c.size())) | |
this(c.comp.asInstanceOf[Comparator[_ >: E]], internal = true, | |
roundUpToPowerOfTwo(c.size() + 1)) // index 0 is unused |
addAll(c) | ||
} | ||
|
||
def this(sortedSet: SortedSet[_ <: E]) = { | ||
this(NaturalComparator.select( | ||
sortedSet.comparator().asInstanceOf[Comparator[_ >: E]]), | ||
internal = true) | ||
internal = true, | ||
roundUpToPowerOfTwo(sortedSet.size())) |
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.
roundUpToPowerOfTwo(sortedSet.size())) | |
roundUpToPowerOfTwo(sortedSet.size() + 1)) // index 0 is unused |
if (LinkingInfo.isWebAssembly) { | ||
val minCapacity = innerImpl.length(inner) + 1 | ||
if (innerImpl.capacity(inner) < minCapacity) | ||
inner = innerImpl.resized(inner, minCapacity) | ||
} |
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 can be moved to JArrayImpl.push
.
The use-case of this function is for ensuring the size of internal buffers (e.g. internal Array of `ju.ArrayList`).
Current js.Array-based PriorityQueue implementation on Wasm requires JS-interop for every operation, and JS-interop is very slow. We use a `linkTimeIf` to select a `scala.Array`-based implementation of internal data structure for PriorityQueue, based on wether it is on JS or WebAssembly.
da5c453
to
83aa955
Compare
I measured performance of The performance change on wasm (red and purple bars) was okay: add/peek gets a bit slower (probably because of array expansion), but the overall performance with poll is much faster. 🎉 The problem is the execution performance on JS. null checks on innnerImplOne problem is that, with Interestingly, without
package helloworld
import java.util.PriorityQueue
object HelloWorld {
def main(args: Array[String]): Unit = {
val pq = new PriorityQueue[Int]()
for (element <- List(1,2,3,4,5)) {
pq.add(element)
}
}
} It's still slowerWith I'm not sure why it's much slower with this diff 🤔 https://gist.github.com/tanishiking/9d0e24de18e994cae5e8742aa9749d98#file-foo2-diff |
471d8b6
to
2ee44af
Compare
Also, store the size of array in the first element of Array
2ee44af
to
0832ba7
Compare
Simliar to ju.ArrayList, this commit adds a Wasm version of internal storage implementation to remove JS interop for better performance.
Branched from #5164
This implementation uses the same technique as parseFloat to simplify having the two implementation for different platforms. see the conversation #5164 (comment)
Also, this is a draft PR for now, because without
linkTimeIf
it seems that the optimizer fails to de-virtualize the function calls of methods onInnerArrayImpl
. Waiting for it5e842d8