- Notifications
You must be signed in to change notification settings - Fork396
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 ourterms of service andprivacy 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?
Uh oh!
There was an error while loading.Please reload this page.
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 thePriorityQueue
6f8093e
toea2171f
CompareThere 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 ofPriorityQueue
, so that instances do not depend on the enclosingPriorityQueue
object.
privatesealedabstractclassInnerArrayImpl { | |
} | |
objectPriorityQueue { | |
privatesealedabstractclassInnerArrayImpl { |
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 oninner
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 newRepr
and a newsize
. 😖
Here is a crazy idea: we have an unused slot at index 0 of the array. What if we use it to store thesize
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 singleRepr
that embodies both the size and the capacity, on Wasm as well as on JS.
tanishikingJun 12, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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 storesize
information 👍
Here I make a prototype implementation for movingInnerArrayImpl
to companion object, and store size toarray(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 ofPriorityQueue
. I think this can be avoided by putting thisprivate val
in the companion object ofPriorityQueue
.
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 ofscala.annotation.*
always come first (afterscala.language.*
imports, if any):
importjava.lang.Utils.roundUpToPowerOfTwo | |
importscala.annotation.tailrec | |
importscala.annotation.tailrec | |
importjava.lang.Utils.roundUpToPowerOfTwo |
Uh oh!
There was an error while loading.Please reload this page.
// 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 onprivate 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 theinternal notion of capacity, which is 1 more than the "public capacity". The public capacity only shows up indef this(initialCapacity)
anddef 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 toJArrayImpl.push
.
da5c453
to83aa955
Comparetanishiking commentedJun 12, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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
packagehelloworldimportjava.util.PriorityQueueobjectHelloWorld {defmain(args:Array[String]):Unit= {valpq=newPriorityQueue[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 |
0832ba7
to144bca0
Comparesjrd commentedJun 16, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I managed to get this to happen by changing privatedefgenApplyTypeApply(tree:Apply,isStat:Boolean): js.Tree= {implicitvalpos= tree.posvalApply(TypeApply(fun@Select(obj, _), targs), args)= treevalsym= fun.symbol symmatch {caseObject_isInstanceOf=> genIsAsInstanceOf(obj, targs, cast=false)caseObject_asInstanceOf=>valtargetTpe= targs.head.tpe objmatch {caseApply(fun,List(cond, thenp, elsep))if fun.symbol== jsDefinitions.LinkingInfo_linkTimeIf&& thenp.tpe<:< targetTpe&& elsep.tpe<:< targetTpe=>valgenObj= genExpr(obj).asInstanceOf[js.LinkTimeIf] js.LinkTimeIf(genObj.cond, genObj.thenp, genObj.elsep)(toIRType(targetTpe))(genObj.pos)case _=> genIsAsInstanceOf(obj, targs, cast=true) }caseObject_synchronized=> genSynchronized(obj, args.head, isStat)case _=> abort("Unexpected type application"+ fun+"[sym:"+ sym.fullName+"]"+" in:"+ tree) } } That removes a spurious (It's a rough draft; it should be cleaned up and tested in order to accept that change into |
The use-case of this function is for ensuring the size of internal buffers (e.g. internal Array of `ju.ArrayList`).
144bca0
toa219596
Comparetanishiking commentedJul 15, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I'm getting back on this. Sorry for the delayed response, and thank you very much for addressing the issue about spurious I found that, another significant performance degradation in Fixing the
Your change in#5168 (comment) looks good to me, maybe we can make another PR with tests? |
} | ||
loop(parent) |
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 is where theloop(parent)
was moved to the wrong place. So the "fixfixUp
" commit should be squashed with this one. It was correct in the original code. ;)
tanishikingJul 16, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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.
Whoa..., I didn't realize I had made this change myself 🤣
When B<:A and C<:A, `linkTimeIf[A](cond){ B }{ C }` wouldgenerate an `.asInstanceOf[A]` that casts the resultingvalue to a supertype.However, this cast is redundant,as the linker will prune one branch at linktime,and the remaining expression is already known to be a compatible type.This commit eliminates the surrounding `.asInstanceOf` around the`linkTimeIf[T]` when both `thenp` and `elsep` are surely subtypes of `T`.context:scala-js#5168Co-authored-by: Sébastien Doeraene <sjrdoeraene@gmail.com>
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.
Also, store the size of array in the first element of Array
Co-authored-by: Sébastien Doeraene <sjrdoeraene@gmail.com>
a219596
to3f504eb
CompareWhen B<:A and C<:A, `linkTimeIf[A](cond){ B }{ C }` wouldgenerate an `.asInstanceOf[A]` that casts the resultingvalue to a supertype.However, this cast is redundant,as the linker will prune one branch at linktime,and the remaining expression is already known to be a compatible type.This commit eliminates the surrounding `.asInstanceOf` around the`linkTimeIf[T]` when both `thenp` and `elsep` are surely subtypes of `T`.The optimizer already routinely performsthis optimization. However, that comes too late for the module instancefield alias analysis performed by `IncOptimizer`. In that case, while thedesugarer removes the `LinkTimeIf`, the extra `AsInstanceOf` preventsaliasing the field. Removing the cast ahead of time in the compiler allowsfield aliases to be recognized in the presence of `LinkTimeIf`s.context:scala-js#5168Co-authored-by: Sébastien Doeraene <sjrdoeraene@gmail.com>
Uh oh!
There was an error while loading.Please reload this page.
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 asparseFloat 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