Skip to content

Conversation

@mroth
Copy link
Owner

@mroth mroth commented Feb 7, 2024

v3 is a breaking change release that migrates to math/rand/v2 and requires Go 1.22 or greater.

Changes from v2

Breaking Changes

  1. Module path: github.com/mroth/weightedrand/v3
  2. Go version: Requires Go 1.22+ (for math/rand/v2)
  3. Removed PickSource: Replaced by PickWith (same functionality using math/rand/v2 source)

Non-Breaking Changes

  1. Internal backing changed to uint64: Allows larger weight sums (addresses Use of int for internal counter #31)
  2. Uses slices.BinarySearch: Replaces manual searchInts implementation

Implementation Tasks

Code Changes

  • Update module path to v3 in go.mod
  • Update Go version requirement to 1.22 in go.mod
  • Migrate imports from math/rand to math/rand/v2
  • Change internal backing from int to uint64
  • Remove deprecated PickSource method
  • Replace sort.Slice with slices.SortFunc
  • Replace manual searchInts with slices.BinarySearch
  • Remove historic example programs
  • Add PickWith(r *rand.Rand) method for explicit custom rand source
  • Simplify Pick() to always use global rand (no conditional)

Tests & Benchmarks

  • Add basic tests for PickWith to verify functionality.
  • Comparative benchmarks between v2 and v3

Documentation & Release

  • Update README: change import path from v2 to v3
  • Update README: change Requirements section (Go 1.22+, link to v2 for older versions)
  • Update README: pkg.go.dev badge URL to v3

Release Sequence

  1. Create v2 branch from main (preserves v2 for maintenance)
  2. Comment on related issues explaining how v3 addresses them:
  3. Merge PR v3 release #36 to main (use "Closes Nit: undeprecate PickSource #29, Use of int for internal counter #31, Feature: Utilize math/rand/v2 #37" in merge commit)
  4. Tag release v3.0.0

Benchmark Comparison (v2 vs v3)

Benchmarks run on Apple M4 Pro, Go 1.24, 10 iterations per benchmark.

Results

                               │ v2 (main)  │              v3 (v3-dev)               │
                               │   sec/op   │    sec/op     vs base                  │
NewChooser/size=1e1-14           98.49n ± 1%   66.09n ± 29%  -32.90% (p=0.000 n=10)
NewChooser/size=1e2-14           405.3n ± 1%   342.6n ±  2%  -15.48% (p=0.000 n=10)
NewChooser/size=1e3-14           3.088µ ± 0%   2.707µ ±  1%  -12.34% (p=0.000 n=10)
NewChooser/size=1e4-14           28.94µ ± 0%   25.01µ ±  2%  -13.58% (p=0.000 n=10)
NewChooser/size=1e5-14           262.6µ ± 1%   243.9µ ±  2%   -7.14% (p=0.000 n=10)
NewChooser/size=1e6-14           2.405m ± 1%   2.291m ±  3%   -4.74% (p=0.000 n=10)
NewChooser/size=1e7-14           28.97m ± 3%   27.25m ±  5%   -5.94% (p=0.000 n=10)
Pick/size=1e1-14                 18.82n ± 7%   18.47n ±  5%        ~ (p=0.353 n=10)
Pick/size=1e2-14                 30.18n ± 0%   29.41n ±  1%   -2.57% (p=0.000 n=10)
Pick/size=1e3-14                 42.22n ± 0%   40.66n ±  0%   -3.68% (p=0.000 n=10)
Pick/size=1e4-14                 54.96n ± 0%   52.76n ±  0%   -4.02% (p=0.000 n=10)
Pick/size=1e5-14                 81.44n ± 0%   77.58n ±  0%   -4.74% (p=0.000 n=10)
Pick/size=1e6-14                 127.0n ± 0%   108.3n ±  1%  -14.72% (p=0.000 n=10)
Pick/size=1e7-14                 251.2n ±15%   231.5n ±  1%   -7.86% (p=0.000 n=10)
PickParallel/size=1e1-14         1.793n ± 7%   1.690n ±  4%   -5.80% (p=0.015 n=10)
PickParallel/size=1e2-14         2.808n ± 2%   2.672n ±  1%   -4.86% (p=0.000 n=10)
PickParallel/size=1e3-14         3.712n ± 2%   3.710n ±  2%        ~ (p=0.868 n=10)
PickParallel/size=1e4-14         4.743n ± 0%   4.789n ±  1%   +0.98% (p=0.000 n=10)
PickParallel/size=1e5-14         6.854n ± 1%   7.064n ±  2%   +3.06% (p=0.000 n=10)
PickParallel/size=1e6-14         10.05n ± 2%   10.71n ±  1%   +6.56% (p=0.000 n=10)
PickParallel/size=1e7-14         22.86n ± 1%   22.68n ±  1%        ~ (p=0.171 n=10)

@trungdlp-wolffun
Copy link

I'm very excited to improve. Can I help you, @mroth?

@mroth
Copy link
Owner Author

mroth commented Aug 16, 2024

I'm very excited to improve. Can I help you, @mroth?

That'd be great! Now that Go 1.23 is released, mostly what remains here is validating the new method for setting a custom randomness source, seen in cd42f24. I'm still not sure about the API here.

@robinjoseph08
Copy link

As a user of v2, I think the API looks good here. As a bonus, it would be nice to rename your NewChooser function to NewChooserWithRand and then make a new simple NewChooser function that just calls NewChooserWithRand with nil as the passed in rand value. I think most people's use-cases will be wanting to set the rand at instantiation, and while you can do it with

c, _ := weightedrand.NewChooser(...)
c.SetRand(r)

it's more convenient to do it all in a single call.

If you're on the fence about including the SetRand method altogether, I personally don't have a major use for it, though I could see it being useful for testing purposes (i.e. swapping out the rand source for each test so that various cases can be tested). Maybe you can leave it off for the first version of v3 (assuming you add the NewChooserWithRand function), and if you get requests for it, you can easily add it back in? It's harder to deprecate/remove functionality than to add it.

Other than that, it all seems reasonable to me, and I'm excited for when it's released!

@mroth
Copy link
Owner Author

mroth commented Oct 23, 2024

As a user of v2, I think the API looks good here.

Thank you for the helpful and thoughtful feedback @robinjoseph08 ! Sorry for the delay in response.

...it would be nice to rename your NewChooser function to NewChooserWithRand and then make a new simple NewChooser function that just calls NewChooserWithRand with nil as the passed in rand value. I think most people's use-cases will be wanting to set the rand at instantiation...

Yeah, I can see that. My worry with the ergonomics is that since NewChooser takes Choices as variadic args (perhaps not the best in retrospect, but I don't know if I should change that at this point), the rand source would have to come first in ordering, and its a bit confusing from an idiomatic perspective to have the "option" come first in the parameter ordering. The alternative would be to have a fairly different signature for NewChooserWithRand entirely, e.g.:

// generic type constraints omitted in examples here to aid in clarity of readability:
func NewChooser(choices ...Choice) (*Chooser, error)
func NewChooserWithRand(choices []Choice, r *rand.Rand) (*Chooser, error)

But this presents other ergonomics issues due to the inconsistency (and I generally do think its better to avoid requiring slice construction in the call site, so end-users to not have to deal with declaring []Choice[T, W]{...} in the callsite.

A third alternative could be to offer a chainable func (c *Chooser) WithRand(r *rand.Rand) *Chooser (or perhaps a WithOptions(opts ...ChooserOption) for future proofing), making this settable at instantiation. This could also be introduced in the future.

If you're on the fence about including the SetRand method altogether, I personally don't have a major use for it, though I could see it being useful for testing purposes (i.e. swapping out the rand source for each test so that various cases can be tested). Maybe you can leave it off for the first version of v3 (assuming you add the NewChooserWithRand function), and if you get requests for it, you can easily add it back in? It's harder to deprecate/remove functionality than to add it.

Good points all. I was hoping to reintroduce some way to do this in v3 (due to requests in #29), but this is currently the design decision that is blocking getting to a release...

mroth and others added 5 commits January 27, 2026 16:57
This a first step in refactoring towards a v3 release.  A major semantic
version release is required in order to let us fully remove the
deprecated PickSource from our API.

Switches to using the new math/rand/v2 module.  Paving the way for the
future.

Removes PickSource method: As planned, this will remove the previously
deprecated PickSource method that uses v1 of math/rand sources. Custom
randomness source should be reintroduced in a future version using a
different methodology (e.g. setting on the Chooser instead of with each
function call).

Switches the Chooser backing from being a system int to a uint64. This
should result in defined behavior across 32 and 64 bit systems (with
the potential for some performance regressions on 32 bit systems, which
I consider an acceptable tradeoff).

Internal implementation: removes the custom searchInts binary sort that
was present for performance (ala github.com/mroth/xsort)in favor of
slices.BinarySearch which is available as of go1.21 and hits acceptable
performance as of go1.22.

goos: darwin
goarch: arm64
pkg: github.com/mroth/weightedrand/v2
                              │ v2-main.txt  │             v3-dev1.txt              │
                              │    sec/op    │    sec/op     vs base                │
NewChooser/size=1e1-8           132.7n ±  0%   132.9n ±  0%       ~ (p=0.314 n=6)
NewChooser/size=1e2-8           476.1n ±  1%   472.8n ±  0%  -0.70% (p=0.002 n=6)
NewChooser/size=1e3-8           3.406µ ±  0%   3.412µ ±  0%       ~ (p=0.379 n=6)
NewChooser/size=1e4-8           31.19µ ±  1%   31.03µ ±  0%  -0.51% (p=0.002 n=6)
NewChooser/size=1e5-8           296.6µ ±  0%   295.9µ ±  0%       ~ (p=0.394 n=6)
NewChooser/size=1e6-8           2.843m ±  1%   2.843m ±  1%       ~ (p=0.485 n=6)
NewChooser/size=1e7-8           35.83m ±  1%   35.92m ±  1%       ~ (p=0.485 n=6)
Pick/size=1e1-8                 22.49n ±  8%   20.28n ±  9%  -9.80% (p=0.015 n=6)
Pick/size=1e2-8                 35.26n ±  2%   32.82n ±  2%  -6.92% (p=0.002 n=6)
Pick/size=1e3-8                 48.41n ±  1%   45.38n ±  1%  -6.26% (p=0.002 n=6)
Pick/size=1e4-8                 63.30n ±  1%   60.23n ±  2%  -4.85% (p=0.002 n=6)
Pick/size=1e5-8                 85.92n ±  1%   82.53n ±  1%  -3.95% (p=0.002 n=6)
Pick/size=1e6-8                 111.5n ±  1%   107.3n ±  4%  -3.72% (p=0.013 n=6)
Pick/size=1e7-8                 240.7n ±  2%   233.2n ±  1%  -3.10% (p=0.002 n=6)
PickParallel/size=1e1-8         2.982n ±  6%   2.760n ±  5%  -7.43% (p=0.009 n=6)
PickParallel/size=1e2-8         4.679n ±  1%   4.360n ±  2%  -6.81% (p=0.002 n=6)
PickParallel/size=1e3-8         6.422n ±  2%   6.059n ±  1%  -5.66% (p=0.002 n=6)
PickParallel/size=1e4-8         8.463n ±  0%   8.114n ± 58%       ~ (p=0.058 n=6)
PickParallel/size=1e5-8         11.55n ±  3%   11.06n ±  0%  -4.24% (p=0.002 n=6)
PickParallel/size=1e6-8         14.98n ±  0%   14.40n ±  0%  -3.87% (p=0.002 n=6)
PickParallel/size=1e7-8         34.70n ±  0%   33.71n ±  0%  -2.82% (p=0.002 n=6)
PickSourceParallel/size=1e1-8   2.752n ± 10%
PickSourceParallel/size=1e2-8   4.369n ±  2%
PickSourceParallel/size=1e3-8   5.989n ±  1%
PickSourceParallel/size=1e4-8   7.991n ±  2%
PickSourceParallel/size=1e5-8   11.28n ±  0%
PickSourceParallel/size=1e6-8   14.59n ±  0%
PickSourceParallel/size=1e7-8   33.86n ±  0%
geomean                         120.0n         279.7n        -3.59%               ¹
¹ benchmark set differs from baseline; geomeans may not be comparable
Since we're already requiring the slices package in stdlib with this
refactor, we can utilize this newer function which should be slightly
more efficient (and has nicer ergonomics imo).

Performs roughly ~11% faster during NewChooser initialization.

goos: darwin
goarch: arm64
pkg: github.com/mroth/weightedrand/v2
                        │  v3-dev1.txt  │             v3-dev2.txt              │
                        │    sec/op     │   sec/op     vs base                 │
NewChooser/size=1e1-8     132.90n ±  0%   70.73n ± 1%  -46.78% (p=0.002 n=6)
NewChooser/size=1e2-8      472.8n ±  0%   444.1n ± 2%   -6.05% (p=0.002 n=6)
NewChooser/size=1e3-8      3.412µ ±  0%   3.333µ ± 0%   -2.30% (p=0.002 n=6)
NewChooser/size=1e4-8      31.03µ ±  0%   30.30µ ± 0%   -2.33% (p=0.002 n=6)
NewChooser/size=1e5-8      295.9µ ±  0%   291.9µ ± 1%   -1.36% (p=0.002 n=6)
NewChooser/size=1e6-8      2.843m ±  1%   2.775m ± 1%   -2.37% (p=0.002 n=6)
NewChooser/size=1e7-8      35.92m ±  1%   32.99m ± 2%   -8.16% (p=0.002 n=6)
geomean                    279.7n         36.41µ       -11.60%               ¹
¹ benchmark set differs from baseline; geomeans may not be comparable
The internal mechanics of this are a bit inelegant, since unfortunately
the global randomness source is not exported, necessitating these nil
check methods instead.

The API here needs some user feedback. I believe the majority case will
want to set this once and not on a per-call basis (cf. the deprecated
PickSource method in the previous version), but that needs be validated.
@mroth
Copy link
Owner Author

mroth commented Jan 28, 2026

After some more thought on the API for custom randomness sources, I've landed on a simpler approach than the SetRand method I had prototyped.

Instead of a setter that adds mutable state to the Chooser, I'm going with a dual-method pattern similar to what oklog/ulid does, and very similar to the deprecated PickSource from v2, except using math/rand/v2 for randomness source:

c.Pick()           // uses global rand (the common case)
c.PickWith(rs)     // uses provided *rand.Rand source

This keeps Chooser stateless/immutable after construction, makes the randomness source explicit at the call site, and avoids any potential for surprising behavior in concurrent code.

For the v2 to v3 migration, users of the deprecated PickSource would just rename to PickWith (and update their rand source construction for math/rand/v2).

I did some research into how other Go libraries handle this pattern (google/uuid, gofrs/uuid, oklog/ulid, gonum/sampleuv) and this felt like the cleanest fit.

Planning to move forward with this approach soon unless anyone has concerns or alternative suggestions.

mroth added 2 commits January 27, 2026 19:18
Replace mutable SetRand() method with immutable PickWith(r *rand.Rand),
following the dual-method pattern used by oklog/ulid and similar libraries.

Addresses #29.
@mroth mroth changed the title Exploratory work towards a potential v3 release v3 release Jan 28, 2026
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.

Nit: undeprecate PickSource

3 participants