Try improve WASM performance.#247
Conversation
|
@tzimnoch , this looks similar to your results. Small improvement which I think is worth merging. |
There was a problem hiding this comment.
What's a good way to benchmark? Shall I have Cursor code one up, perhaps by adding a --wasm option to dtest?
Never mind - I had Cursor create a new wasm-dtest utility that wraps node calls. I have a PR out for that. When I have a chance I'll cherry pick this PR in and see what difference it makes.
|
@tameware, I think it makes sense to merge the |
|
Initial benchmarks show a 4.5% performance improvement over 100 calcs (2,000 solves). I'll run against 1,000 calcs next, to make sure. |
|
Interesting! 100 calcs comparison was 9815 / 10274 ms, for 0.9553. For 1,000 calcs it's 126298 / 126993 = 0.99452726. Still faster, but only by 0.5%. I'll run a few more to make sure this is not just noise. More important, I've been comparing against develop head. That's wrong - I should be comparing the code before and after this commit. |
|
Proper comparison now: I'll run one more with a larger sample size, then approve if the results hold up. |
|
Disappointing. Back to the drawing board? I'll run a few more times to confirm. |
|
I found and fixed an int32 overflow bug in the WASM dtest code, but it did not affect the results. list10000.txt contains more than half duplicated deals, and it's possible that affected the results. I switched to running through list1000.txt 10 times. Clearly that also has duplicates, but they are not batched the way they are in list10000.txt. My first run with the new setup was favorable to this PR: |
dds_mvp_calc_table called the stateless CalcDDtablePBN, which allocated a fresh SolverContext (and transposition table) on every call and freed it before returning. Reuse a lazily-constructed static SolverContext for the lifetime of the wasm module instance instead, calling reset_for_solve() between deals to recycle the TT memory pool without freeing it. Co-Authored-By: Claude <noreply@anthropic.com>
Adds kPbnHand1/kExpectedHand1 (sourced from hands/list10.txt's third record) and a test that solves two different deals back-to-back through the same reused static SolverContext, then re-solves the first deal to confirm no stale state leaks across calls. Co-Authored-By: Claude <noreply@anthropic.com>
Replace -fexceptions (Emscripten's JS-trampoline exception emulation) with -fwasm-exceptions (native wasm EH proposal, supported by emsdk 5.0.7 / all current browsers and Node) at compile and link time for all wasm builds. The toolchain's default -fno-exceptions is not overridden at the clang frontend level by -fwasm-exceptions alone in this LLVM build, so -fexceptions is kept immediately before it in DDS_CPPOPTS to force-enable exceptions before -fwasm-exceptions selects the wasm EH lowering mechanism. Benchmarked ~4% faster (90-91ms/call vs 93-94.5ms/call) over a rotating set of deals via Node timing script. Co-Authored-By: Claude <noreply@anthropic.com>
Reflects -fexceptions -> -fwasm-exceptions and documents why wasm link-time -flto was tried and reverted (the pinned emsdk 5.0.7 frozen cache lacks LTO-bitcode system libraries). Co-Authored-By: Claude <noreply@anthropic.com>
web-mvp: records that dds_mvp_calc_table now reuses one static SolverContext for the module's lifetime (Task 01) instead of allocating fresh per call, including the concurrency caveat. wasm-emscripten: adds a known-gap entry for link-time LTO, tried and reverted because the pinned emsdk 5.0.7 toolchain's frozen cache lacks LTO-bitcode system libraries. build-system: corrects the "macOS and WASM add LTO" claim to reflect that WASM only gets compile-time LTO, not link-time. Co-Authored-By: Claude <noreply@anthropic.com>
7fe479b to
8f6e046
Compare
|
Do the changes in the compared branches differ by the code to sort hands by difficulty? (#216) Is there any possibility that the cause of slowdown with increasing numbers of hands is that sorting the hands becomes more expensive than the threading gains as the number of hands increases? I'd be surprised if this explains the difference, but it's not out of the realm of possibility. The results imply there's some non-linear-time operation happening in the After branch. Sorting is usually O(n log n). Was there any evaluation of the cost to sort? If it is expensive, a complete ordering might not be necessary. Setting a threshold such that the the top ~20% most difficult hands are solved before the bottom 80% (in any order) may be sufficient, which would be an O(n) operation. |
Both branches I'm testing contain PR 216. The only difference is the changes in this PR.
The code works in batches of 40 deals. I cannot think the sort cost is significant, but it would be easy to measure.
No. I benchmarked throughput before and after the change. Note that the difficulty sort replaced Scheduler code that was present in 2.9 but not used in 3.0.
Agreed - if sorting is expensive then a partial order would be worth considering. |
|
A second extensive test with no duplicates within batches shows similar improvement to the first. I'd say this PR is good to go. |
There was a problem hiding this comment.
Pull request overview
This PR targets a measured WASM performance regression by (1) making the Web MVP bridge reuse a long-lived SolverContext instead of re-allocating per call and (2) switching the Emscripten build to native WebAssembly exception handling, with accompanying documentation/spec updates to reflect the current WASM constraints (notably the lack of link-time LTO under the pinned hermetic emsdk).
Changes:
- Reuse a module-lifetime
static SolverContextindds_mvp_calc_table, resetting between deals to recycle TT memory without reallocating. - Enable native WASM exception handling by using
-fwasm-exceptions(and ensuring exceptions are enabled at compile-time via-fexceptionsin WASM copts). - Add a regression test for cross-deal context reuse and update specs/docs to document the new behavior and toolchain limitations.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| web/dds_mvp_wasm.cpp | Reuses a session-scoped SolverContext and routes table computation through the context-aware C++ API. |
| web/dds_mvp_wasm_test.cpp | Adds a regression test verifying correct results across multiple solves using the reused context. |
| wasm_compat.bzl | Switches shared Emscripten link flags from legacy exception handling to native wasm EH. |
| CPPVARIABLES.bzl | Ensures WASM builds enable exceptions and select wasm EH lowering (-fexceptions + -fwasm-exceptions). |
| specs/web-mvp.md | Documents the session-scoped context behavior and its concurrency implications. |
| specs/wasm-emscripten.md | Documents the current “no link-time LTO” constraint under the pinned hermetic emsdk cache model. |
| specs/build-system.md | Clarifies platform-specific optimization/LTO behavior, especially for WASM vs macOS. |
| docs/wasm_build.md | Updates user-facing WASM flag documentation and adds the link-time LTO limitation note. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
I thought I had approved. The only active button I see now is "Merge pull request". |
Summary
Investigates and partially fixes the WASM performance regression flagged in
claude/analysis/clang_wasm.md. The analysis's primary hypothesis(per-call
SolverContextallocation) turned out not to be the actualbottleneck once measured — but the fix is architecturally correct and kept
regardless. The secondary factor (Emscripten's legacy exception emulation)
turned out to be the one that actually moved the needle.
SolverContextin the browser MVP —dds_mvp_calc_tableused to construct and tear down a fresh
SolverContext(and itstransposition table) on every call. It now reuses one
static SolverContextfor the module's lifetime, calling
reset_for_solve()between deals.Measured: no significant change (~93-94ms/call before and after) — the
double-dummy search itself dominates the workload, not the per-call
allocation. Kept anyway since it matches the previous session-singleton
design and is the architecturally correct behavior for a long-lived module.
-fexceptions(JS-trampolineexception emulation) to
-fwasm-exceptions(native wasm EH proposal) atcompile and link time. Measured ~4% faster (90-91ms/call vs.
93-94.5ms/call), consistent across runs.
-msimd128was tried and reverted — no improvement (slight regression,~92-93ms/call vs. 90-91ms/call without it).
-fltofor WASM was tried and reverted — the pinnedemsdk 5.0.7toolchain ships a frozen, pre-built cache with only non-LTO systemlibraries; requesting link-time LTO needs LTO-bitcode variants of core
sysroot libs (e.g.
libprintf_long_double) that the frozen cache can'tbuild inside Bazel's hermetic sandbox. Documented as a known gap rather than
worked around.
web-mvp,wasm-emscripten,build-system) anddocs/wasm_build.mdupdated to reflect the above.
What did not work (documented, not silently dropped)
SolverContextreuse-msimd128-fltoTest plan
bazel test //web:dds_mvp_wasm_test— includes a new cross-dealcontext-reuse regression test (
FillsFlatStrainHandTableAcrossReuse)bazel test //web:web_tests //web:web_system_testsbazel test --test_tag_filters=e2e //web:dds_mvp_e2e_test(Playwright)bazel test //wasm:allbazel test //library/tests/...(sanity — no native API changes)bazel build //...