Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Open
tanishiking wants to merge4 commits intoscala-js:main
base:main
Choose a base branch
Loading
fromtanishiking:priorityqueue-wasm

Conversation

tanishiking
Copy link
Contributor

@tanishikingtanishiking commentedMay 15, 2025
edited
Loading

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 withoutlinkTimeIf it seems that the optimizer fails to de-virtualize the function calls of methods onInnerArrayImpl. Waiting for it
5e842d8


// 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
Copy link
Member

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. 😅

Copy link
ContributorAuthor

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

@tanishikingtanishikingforce-pushed thepriorityqueue-wasm branch 4 times, most recently from6f8093e toea2171fCompareMay 17, 2025 04:36
@tanishikingtanishiking marked this pull request as ready for reviewMay 17, 2025 05:09
@tanishikingtanishiking requested a review fromsjrdJune 6, 2025 05:50
Copy link
Member

@sjrdsjrd left a 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 {
Copy link
Member

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.

Suggested change
privatesealedabstractclassInnerArrayImpl {
}
objectPriorityQueue {
privatesealedabstractclassInnerArrayImpl {

Copy link
Member

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 newReprand 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.

Copy link
ContributorAuthor

@tanishikingtanishikingJun 12, 2025
edited
Loading

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 =
Copy link
Member

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
Copy link
Member

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):

Suggested change
importjava.lang.Utils.roundUpToPowerOfTwo
importscala.annotation.tailrec
importscala.annotation.tailrec
importjava.lang.Utils.roundUpToPowerOfTwo

Comment on lines 102 to 103
// The index 0 is not used; the root is at index 1.
// This is standard practice in binary heaps, to simplify arithmetics.
Copy link
Member

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
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
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()))
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
}, internal=true, roundUpToPowerOfTwo(c.size()))
}, internal=true, roundUpToPowerOfTwo(c.size()+1))// index 0 is unused

tanishiking reacted with thumbs up emoji
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()))
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
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()))
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
roundUpToPowerOfTwo(sortedSet.size()))
roundUpToPowerOfTwo(sortedSet.size()+1))// index 0 is unused

tanishiking reacted with thumbs up emoji
Comment on lines +110 to +94
if (LinkingInfo.isWebAssembly) {
val minCapacity = innerImpl.length(inner) + 1
if (innerImpl.capacity(inner) < minCapacity)
inner = innerImpl.resized(inner, minCapacity)
}
Copy link
Member

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.

@tanishiking
Copy link
ContributorAuthor

tanishiking commentedJun 12, 2025
edited
Loading

I measured performance ofPriorityQueueusing a simple priority-queue micro benchmark with fastLinkJS. It does add/peek/poll 10000 numbers (in order from 0-10000, maybe inappropriate for priorityqueue.)

download

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.
As you can see from the blue (main) and orange (this change) graphs, the performance on add/peek is about 10 times slower, andpoll is also degraded by 2 times.

null checks on innnerImpl

One problem is that, withlinktimeIf, the null check oninnerImpl has inserted everywhere$n($m_ju_PriorityQueue$().ju_PriorityQueue$__f_java$util$PriorityQueue$$innerImpl); The diff between main vs this branch, generated from the Scala code below:https://gist.github.com/tanishiking/9d0e24de18e994cae5e8742aa9749d98#file-foo-diff

Interestingly, withoutlinkeTimeIf on innerImplprivate val innerImpl: InnerArrayImpl = InnerArrayImpl.JSArrayImpl (green bar),innerImpl gets inlined and there's no null check on it. diff:https://gist.github.com/tanishiking/9d0e24de18e994cae5e8742aa9749d98#file-foo2-diff

linktimeIf should generate the same code as theinnerImpl = InnerArrayImpl.JSArrayImpl.

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 slower

Withprivate val innerImpl: InnerArrayImpl = InnerArrayImpl.JSArrayImpl, the performance gets better (green bar), but it's still much slower thanmain.

I'm not sure why it's much slower with this diff 🤔https://gist.github.com/tanishiking/9d0e24de18e994cae5e8742aa9749d98#file-foo2-diff

@tanishikingtanishikingforce-pushed thepriorityqueue-wasm branch 4 times, most recently from0832ba7 to144bca0CompareJune 16, 2025 12:33
@sjrd
Copy link
Member

sjrd commentedJun 16, 2025
edited
Loading

linktimeIf should generate the same code as theinnerImpl = InnerArrayImpl.JSArrayImpl.

I managed to get this to happen by changinggenApplyTypeApply inGenJSCode.scala to

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 spuriousjs.AsInstanceOf around thejs.LinkTimeIf, which prevents the module aliasing analysis done inIncOptimizer.scala.

(It's a rough draft; it should be cleaned up and tested in order to accept that change intomain.)

tanishiking reacted with heart emoji

The use-case of this function is for ensuring the size of internal buffers (e.g. internal Array of `ju.ArrayList`).
@tanishiking
Copy link
ContributorAuthor

tanishiking commentedJul 15, 2025
edited
Loading

I'm getting back on this. Sorry for the delayed response, and thank you very much for addressing the issue about spuriousjs.AsInstanceOfs!

I found that, another significant performance degradation inadd is caused byfixUp implementation. It always traversed all the way up to the root node even after finding a suitable location. fixed indbfd7a7
(Interestingly, it wasn't a problem withoutinnerImpl, because JS will be optimized to stop traversing whenfixUp found the location).

Fixing thefixUp and applying your change, we could optimize Wasm without degrade the JS performance 🎉
Now there's a small diff between main and HEAD in JShttps://gist.github.com/tanishiking/5f7b24a1e4e3e724b85d5e60415bf1e4

Benchmark(ms)JS (HEAD)JS (main)Wasm (HEAD)Wasm (main)
add_N65.8270.81232.06705.57
poll_N_FromFull1557.521537.544025.598748.01
peek_Repeatedly83.2581.29289.831077.45
build_FromCollection_N77.4282.55285.58721.59
pipelined_AddPoll1724.261529.603539.568522.60

Your change in#5168 (comment) looks good to me, maybe we can make another PR with tests?

}
loop(parent)
Copy link
Member

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. ;)

Copy link
ContributorAuthor

@tanishikingtanishikingJul 16, 2025
edited
Loading

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 🤣

tanishiking added a commit to tanishiking/scala-js that referenced this pull requestJul 16, 2025
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>
tanishikingand others added3 commitsJuly 16, 2025 15:45
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>
tanishiking added a commit to tanishiking/scala-js that referenced this pull requestJul 18, 2025
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`.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>
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@sjrdsjrdsjrd requested changes

Requested changes must be addressed to merge this pull request.

Assignees
No one assigned
Labels
None yet
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

2 participants
@tanishiking@sjrd

[8]ページ先頭

©2009-2025 Movatter.jp