Skip to content

perf(quickcheck): execute zipped generators directly - #4147

Merged
bobzhang merged 1 commit into
moonbitlang:mainfrom
mizchi:perf/quickcheck-direct-zip
Sep 7, 2026
Merged

perf(quickcheck): execute zipped generators directly#4147
bobzhang merged 1 commit into
moonbitlang:mainfrom
mizchi:perf/quickcheck-direct-zip

Conversation

@mizchi

@mizchi mizchi commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Implement Generator::zip_with and zip_with3 directly instead of composing flat_map and map. The previous composition allocated temporary generators and closures for every generated sample.

The state splitting and evaluation order are preserved.

Native benchmark (zip_with, 1,000,000 samples):

before after change
38.66 ms 11.64 ms 70% faster

Validation: all 82 QuickCheck tests pass on wasm, wasm-gc, JS, and native; moon check --target all and moon info pass.

Copilot AI lite review requested due to automatic review settings August 24, 2026 18:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes @quickcheck.Generator composition by implementing Generator::zip_with and Generator::zip_with3 directly, avoiding per-sample allocation overhead from flat_map/map composition while preserving the same RandomState::split() sequencing semantics.

Changes:

  • Implement Generator::zip_with via a single generator closure that splits state once and runs both generators directly.
  • Implement Generator::zip_with3 directly, matching the previous nested-flat_map state-splitting behavior.
  • Add a benchmark-style test for Generator::zip_with and wire in the bench dependency for tests.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
quickcheck/moon.pkg Adds moonbitlang/core/bench as a test-only import to support benchmark tests.
quickcheck/generator.mbt Replaces composed flat_map/map implementations of zip_with/zip_with3 with direct implementations to reduce allocation/closure overhead.
quickcheck/generator_bench_test.mbt Adds a benchmark test exercising Generator::zip_with for performance tracking.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@coveralls

coveralls commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 6572

Coverage increased (+0.004%) to 89.244%

Details

  • Coverage increased (+0.004%) from the base build.
  • Patch coverage: 10 of 10 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 18288
Covered Lines: 16321
Line Coverage: 89.24%
Coverage Strength: 275156.61 hits per line

💛 - Coveralls

@bobzhang
bobzhang force-pushed the perf/quickcheck-direct-zip branch from 41cf30a to c511bfa Compare September 7, 2026 09:28
@bobzhang

bobzhang commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Reviewed and verified locally on c511bfa1. This is correct and the numbers hold up — queued for auto-merge once CI finishes.

The equivalence claim checks out. I diffed the new implementations against the old flat_map compositions over 40 seeds × 7 sizes × 25 samples for both zip_with and zip_with3, plus Arbitrary-backed generators and nested zip_with/zip_with3 compositions: byte-identical throughout, on all four backends. The test is sensitive to exactly the thing that could break — changing second_state.split() to state.split() in zip_with3 fails 3 of those cases.

The one real reordering is in zip_with3: the second split now happens before self.run, where the old nesting did it after. That is safe because RandomState::split advances the parent's seed and returns a new record ({ seed: mix64(seed1), gamma: mix_gamma(self.seed) }), so parent and child never alias and the two operations commute.

One thing worth recording, since the value-level diff cannot see it: combine(self.run(..), other.run(..)) makes the run order depend on argument evaluation order, where the old nesting fixed it structurally. For pure generators this is unobservable, since the states are disjoint. For an effectful generator it is observable, so I checked it directly with generators that append to a shared trace — ["a", "b"] and ["a", "b", "c"] on wasm, wasm-gc, js and native. MoonBit evaluates arguments left to right, so effectful generators still fire in the same order as before.

Numbers reproduce. bench Generator::zip_with n=1000000, native release, on an M-series mac: 35.15 ms → 9.46 ms, a 73% reduction — the same shape as the 38.66 → 11.64 ms in the description. zip delegates to zip_with, so it inherits the win.

Everything else is clean. moon test -p quickcheck --target all 157/157 on wasm, wasm-gc, js and native; moon check --target all, moon fmt --check and moon info all clean with no .mbti change; and the new benchmark runs on all four targets, not just native.

Only nit, not worth holding the PR for: there is a benchmark for zip_with but none for zip_with3, which got the larger structural change.

@bobzhang
bobzhang enabled auto-merge (squash) September 7, 2026 09:35
@bobzhang
bobzhang merged commit 3d826be into moonbitlang:main Sep 7, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants