10000 Experiment: Avoid `JSArrayConstr` for Varargs to optimize the Wasm backend by tanishiking · Pull Request #5148 · scala-js/scala-js · GitHub
[go: up one dir, main page]

Skip to content

Experiment: Avoid JSArrayConstr for Varargs to optimize the Wasm backend #5148

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

Closed
wants to merge 1 commit into from

Conversation

tanishiking
Copy link
Contributor
@tanishiking tanishiking commented Mar 26, 2025

This is more like an experiment report for discuss based on the results of this change.


Currently in Scala.js, varargs call like List(1, 2, 3), it is translated into the IR form js.WrappedArray(JSArrayConstr(...)). That requires JS interop for constructing the array and accessing its elements. Since Wasm-to-JS calls are expensive, this is undesirable for performance.

This commit experiments avoiding JSArrayConstr for varargs. Instead, varargs are transformed into something like new WrappedArray$ofInt(ArrayValue(1, 2, 3)) (or new ArraySeq$ofInt(...) on 2.13) to explore potential Wasm-specific optimizations.

Note1: While reducing JS interop can improve performance on the Wasm backend, the same does not apply the JS backend. We'd need to re-optimize back to JSArrayConstr-based code during the Optimizer for the JS backend.

Note 2: How about WrappedArray.make instead of directly instantiating specialized WrappedArray? I found that runtime type checks in make appear to be very slow, and in some micro-benchmarks, using make performed slightly worse than the original JSArrayConstr-based code.


Unfortunately, the performance improvements were negligible.

For example, in the following code:

def main(args: Array[String]): Unit = {
  val startTime = System.nanoTime()
  val xs = Seq(1, 2, ..., 20)
  xs.foreach(x => assert(x > 0))
  val endTime = System.nanoTime()
  println(s"elapsed: ${endTime - startTime} ns")
}

Both versions (with and without JSArrayConstr) reported similar timings of ~540000–580000 ns.

Benchmarks run using sjrd/scalajs-benchmarks/wasm also did not show any significant performance differences.

Benchmark before after Ratio (after / before)
sha512 12403.95816 12737.42497 1.0269
sha512int 12259.02363 13313.69655 1.0860
queens 2.954778067 2.920396237 0.9873
list 60.56316878 60.52163829 0.9993
richards 87.49714448 87.77807725 1.0032
cd 32866.35461 31672.2486 0.9637
gcbench 104672.8678 121351.2553 1.1588
tracerFloat 870.5680015 876.4962162 1.0068
tracer 784.3968099 783.2297365 0.9985
sudoku 3634.165046 3609.813857 0.9933
nbody 23722.03084 24192.39211 1.0198
permute 262.9023071 265.949228 1.0116
deltaBlue 525.4683864 511.8722724 0.9742
kmeans 206339.5187 202615.0022 0.9820
brainfuck 2352.518883 2357.064959 1.0019
json 288.1723513 280.1001847 0.9720
bounce 33.12443674 33.01821262 0.9978

(It might be because there's not so much varargs in the benchmark)

There may still be room for further optimization in the non-JSArrayConstr implementation.

… Backend

Currently in Scala.js, varargs call like `List(1, 2, 3)`, it is translated into the IR form `js.WrappedArray(JSArrayConstr(...))`. That requires JS interop for constructing the array and accessing its elements.
Since Wasm-to-JS calls are expensive, this is undesirable for performance.

This commit experiments avoiding `JSArrayConstr` for varargs.
Instead, varargs are transformed into something like `new WrappedArray$ofInt(ArrayValue(1, 2, 3))` (or `new ArraySeq$ofInt(...)` on 2.13) to explore potential Wasm-specific optimizations.

Note1: While reducing JS interop can improve performance on the Wasm backend, the same does not apply the JS backend.
We'd need to re-optimize back to `JSArrayConstr`-based code during the Optimizer for the JS backend.

Note 2: How about `WrappedArray.make` instead of directly instantiating specialized `WrappedArray`?
I found that runtime type checks in `make` appear to be very slow, and in some micro-benchmarks, using `make` performed slightly worse than the original `JSArrayConstr`-based code.

---

Unfortunately, the performance improvements were negligible.

For example, in the following code:

```scala
def main(args: Array[String]): Unit = {
  val startTime = System.nanoTime()
  val xs = Seq(1, 2, ..., 20)
  xs.foreach(x => assert(x > 0))
  val endTime = System.nanoTime()
  println(s"elapsed: ${endTime - startTime} ns")
}
```

Both versions (with and without `JSArrayConstr`) reported similar timings of ~540000–580000 ns.

Benchmarks run using [`sjrd/scalajs-benchmarks/wasm`](https://github.com/sjrd/scalajs-benchmarks/tree/wasm) also did not show any significant performance differences.

| Benchmark   | before       | after      | Ratio (after / before) |
|-------------|--------------|--------------|----------------------|
| sha512      | 12403.95816  | 12737.42497  | 1.0269               |
| sha512int   | 12259.02363  | 13313.69655  | 1.0860               |
| queens      | 2.954778067  | 2.920396237  | 0.9873               |
| list        | 60.56316878  | 60.52163829  | 0.9993               |
| richards    | 87.49714448  | 87.77807725  | 1.0032               |
| cd          | 32866.35461  | 31672.2486   | 0.9637               |
| gcbench     | 104672.8678  | 121351.2553  | 1.1588               |
| tracerFloat | 870.5680015  | 876.4962162  | 1.0068               |
| tracer      | 784.3968099  | 783.2297365  | 0.9985               |
| sudoku      | 3634.165046  | 3609.813857  | 0.9933               |
| nbody       | 23722.03084  | 24192.39211  | 1.0198               |
| permute     | 262.9023071  | 265.949228   | 1.0116               |
| deltaBlue   | 525.4683864  | 511.8722724  | 0.9742               |
| kmeans      | 206339.5187  | 202615.0022  | 0.9820               |
| brainfuck   | 2352.518883  | 2357.064959  | 1.0019               |
| json        | 288.1723513  | 280.1001847  | 0.9720               |
| bounce      | 33.12443674  | 33.01821262  | 0.9978               |

There may still be room for further optimization in the non-`JSArrayConstr` implementation.
@tanishiking tanishiking changed the title [DO NOT MERGE] Experiment: Avoiding JSArrayConstr for Varargs to optimize the Wasm backend Experiment: Avoiding JSArrayConstr for Varargs to optimize the Wasm backend Mar 26, 2025
@tanishiking tanishiking changed the title Experiment: Avoiding JSArrayConstr for Varargs to optimize the Wasm backend Experiment: Avoid JSArrayConstr for Varargs to optimize the Wasm backend Mar 26, 2025
@tanishiking tanishiking closed this Apr 9, 2025
@tanishiking tanishiking deleted the array-varargs branch April 9, 2025 00:09
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.

1 participant
0