Skip to content

Commit

Permalink
Merge pull request #28 from mroth/go1.21
Browse files Browse the repository at this point in the history
Updates for go1.21 rand changes
  • Loading branch information
mroth authored Aug 13, 2023
2 parents 62d9872 + b306b18 commit 38f771d
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 81 deletions.
1 change: 1 addition & 0 deletions examples/compbench/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
results.txt
12 changes: 12 additions & 0 deletions examples/compbench/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.PHONY: analysis clean
FILE=results.txt
COUNT=10

analysis: $(FILE)
benchstat -filter=".name:Multiple" -col="/lib" -row="/size" -table="/concurrency" $<

clean:
rm -f $(FILE)

$(FILE):
go test -bench=. -count=${COUNT} | tee $@
133 changes: 69 additions & 64 deletions examples/compbench/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
package compbench

import (
"fmt"
"math"
"math/rand"
"strconv"
"testing"
"time"

"github.com/jmcvetta/randutil"
"github.com/mroth/weightedrand/v2"
Expand All @@ -16,86 +16,85 @@ const BMMinChoices = 10
const BMMaxChoices = 10_000_000

func BenchmarkMultiple(b *testing.B) {
b.Run("jmc_randutil", func(b *testing.B) {
for n := BMMinChoices; n <= BMMaxChoices; n *= 10 {
b.Run(strconv.Itoa(n), func(b *testing.B) {
choices := convertChoices(b, mockChoices(b, n))
b.ResetTimer()
for i := 0; i < b.N; i++ {
randutil.WeightedChoice(choices)
}
})
}
})

b.Run("weightedrand", func(b *testing.B) {
for n := BMMinChoices; n <= BMMaxChoices; n *= 10 {
b.Run(strconv.Itoa(n), func(b *testing.B) {
choices := mockChoices(b, n)
chs, err := weightedrand.NewChooser(choices...)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
chs.Pick()
}
for n := BMMinChoices; n <= BMMaxChoices; n *= 10 {
b.Run(fmt.Sprintf("size=%s", fmt1eN(n)), func(b *testing.B) {
wr_choices := mockChoices(b, n)
ru_choices := convertChoices(b, wr_choices)

b.Run("concurrency=single", func(b *testing.B) {
b.Run("lib=randutil", func(b *testing.B) {
for i := 0; i < b.N; i++ {
randutil.WeightedChoice(ru_choices)
}
})

b.Run("lib=weightedrand", func(b *testing.B) {
chs, err := weightedrand.NewChooser(wr_choices...)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()

for i := 0; i < b.N; i++ {
chs.Pick()
}
})
})
}
})

b.Run("wr-parallel", func(b *testing.B) {
for n := BMMinChoices; n <= BMMaxChoices; n *= 10 {
b.Run(strconv.Itoa(n), func(b *testing.B) {
choices := mockChoices(b, n)
chs, err := weightedrand.NewChooser(choices...)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
rs := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
for pb.Next() {
chs.PickSource(rs)

b.Run("concurrency=parallel", func(b *testing.B) {
b.Run("lib=weightedrand", func(b *testing.B) {
chs, err := weightedrand.NewChooser(wr_choices...)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()

b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
chs.Pick()
}
})
})

b.Run("lib=randutil", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
randutil.WeightedChoice(ru_choices)
}
})
})
})
}
})
})
}
}

// The single usage case is an anti-pattern for the intended usage of this
// library. Might as well keep some optional benchmarks for that to illustrate
// the point.
// THE SINGLE USAGE CASE IS AN ANTI-PATTERN FOR THE INTENDED USAGE OF THIS
// LIBRARY. Provide some optional benchmarks for that to illustrate the point.
func BenchmarkSingle(b *testing.B) {
if testing.Short() {
b.Skip()
}

b.Run("jmc_randutil", func(b *testing.B) {
for n := BMMinChoices; n <= BMMaxChoices; n *= 10 {
b.Run(strconv.Itoa(n), func(b *testing.B) {
choices := convertChoices(b, mockChoices(b, n))
b.ResetTimer()
for n := BMMinChoices; n <= BMMaxChoices; n *= 10 {
b.Run(fmt.Sprintf("size=%s", fmt1eN(n)), func(b *testing.B) {
wr_choices := mockChoices(b, n)
ru_choices := convertChoices(b, wr_choices)

b.Run("lib=randutil", func(b *testing.B) {
for i := 0; i < b.N; i++ {
randutil.WeightedChoice(choices)
randutil.WeightedChoice(ru_choices)
}
})
}
})

b.Run("weightedrand", func(b *testing.B) {
for n := BMMinChoices; n <= BMMaxChoices; n *= 10 {
b.Run(strconv.Itoa(n), func(b *testing.B) {
choices := mockChoices(b, n)
b.ResetTimer()

b.Run("lib=weightedrand", func(b *testing.B) {
for i := 0; i < b.N; i++ {
chs, _ := weightedrand.NewChooser(choices...)
// never actually do this, this is not how the library is used
chs, _ := weightedrand.NewChooser(wr_choices...)
chs.Pick()
}
})
}
})
})
}
}

func mockChoices(tb testing.TB, n int) []weightedrand.Choice[rune, uint] {
Expand All @@ -118,3 +117,9 @@ func convertChoices(tb testing.TB, cs []weightedrand.Choice[rune, uint]) []randu
}
return res
}

// fmt1eN returns simplified order of magnitude scientific notation for n,
// e.g. "1e2" for 100, "1e7" for 10 million.
func fmt1eN(n int) string {
return fmt.Sprintf("1e%d", int(math.Log10(float64(n))))
}
2 changes: 1 addition & 1 deletion examples/compbench/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/mroth/weightedrand/examples/compbench

go 1.18
go 1.21

require (
github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff
Expand Down
18 changes: 11 additions & 7 deletions weightedrand.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
// each element to be selected not being equal, but defined by relative
// "weights" (or probabilities). This is called weighted random selection.
//
// Compare this package with (github.com/jmcvetta/randutil).WeightedChoice,
// which is optimized for the single operation case. In contrast, this package
// creates a presorted cache optimized for binary search, allowing for repeated
// selections from the same set to be significantly faster, especially for large
// data sets.
// This package creates a presorted cache optimized for binary search, allowing
// for repeated selections from the same set to be significantly faster,
// especially for large data sets.
package weightedrand

import (
Expand Down Expand Up @@ -93,7 +91,7 @@ var (

// Pick returns a single weighted random Choice.Item from the Chooser.
//
// Utilizes global rand as the source of randomness.
// Utilizes global rand as the source of randomness. Safe for concurrent usage.
func (c Chooser[T, W]) Pick() T {
r := rand.Intn(c.max) + 1
i := searchInts(c.totals, r)
Expand All @@ -109,6 +107,10 @@ func (c Chooser[T, W]) Pick() T {
//
// It is the responsibility of the caller to ensure the provided rand.Source is
// free from thread safety issues.
//
// Deprecated: Since go1.21 global rand no longer suffers from lock contention
// when used in multiple high throughput goroutines, as long as you don't
// manually seed it. Use [Chooser.Pick] instead.
func (c Chooser[T, W]) PickSource(rs *rand.Rand) T {
r := rs.Intn(c.max) + 1
i := searchInts(c.totals, r)
Expand All @@ -122,7 +124,9 @@ func (c Chooser[T, W]) PickSource(rs *rand.Rand) T {
// overhead.
//
// Thus, this is essentially manually inlined version. In our use case here, it
// results in a up to ~33% overall throughput increase for Pick().
// results in a significant throughput increase for Pick.
//
// See also github.com/mroth/xsort.
func searchInts(a []int, x int) int {
// Possible further future optimization for searchInts via SIMD if we want
// to write some Go assembly code: http://0x80.pl/articles/simd-search.html
Expand Down
38 changes: 29 additions & 9 deletions weightedrand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package weightedrand

import (
"fmt"
"math"
"math/rand"
"strconv"
"sync"
"testing"
"time"
Expand Down Expand Up @@ -33,10 +33,6 @@ func Example() {
* Tests
*******************************************************************************/

func init() {
rand.Seed(time.Now().UTC().UnixNano()) // only necessary prior to go1.20
}

const (
testChoices = 10
testIterations = 1000000
Expand Down Expand Up @@ -213,11 +209,11 @@ func verifyFrequencyCounts(t *testing.T, counts map[int]int, choices []Choice[in
*******************************************************************************/

const BMMinChoices = 10
const BMMaxChoices = 1000000
const BMMaxChoices = 10_000_000

func BenchmarkNewChooser(b *testing.B) {
for n := BMMinChoices; n <= BMMaxChoices; n *= 10 {
b.Run(strconv.Itoa(n), func(b *testing.B) {
b.Run(fmt.Sprintf("size=%s", fmt1eN(n)), func(b *testing.B) {
choices := mockChoices(n)
b.ResetTimer()

Expand All @@ -230,7 +226,7 @@ func BenchmarkNewChooser(b *testing.B) {

func BenchmarkPick(b *testing.B) {
for n := BMMinChoices; n <= BMMaxChoices; n *= 10 {
b.Run(strconv.Itoa(n), func(b *testing.B) {
b.Run(fmt.Sprintf("size=%s", fmt1eN(n)), func(b *testing.B) {
choices := mockChoices(n)
chooser, err := NewChooser(choices...)
if err != nil {
Expand All @@ -247,7 +243,25 @@ func BenchmarkPick(b *testing.B) {

func BenchmarkPickParallel(b *testing.B) {
for n := BMMinChoices; n <= BMMaxChoices; n *= 10 {
b.Run(strconv.Itoa(n), func(b *testing.B) {
b.Run(fmt.Sprintf("size=%s", fmt1eN(n)), func(b *testing.B) {
choices := mockChoices(n)
chooser, err := NewChooser(choices...)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = chooser.Pick()
}
})
})
}
}

func BenchmarkPickSourceParallel(b *testing.B) {
for n := BMMinChoices; n <= BMMaxChoices; n *= 10 {
b.Run(fmt.Sprintf("size=%s", fmt1eN(n)), func(b *testing.B) {
choices := mockChoices(n)
chooser, err := NewChooser(choices...)
if err != nil {
Expand All @@ -274,3 +288,9 @@ func mockChoices(n int) []Choice[rune, int] {
}
return choices
}

// fmt1eN returns simplified order of magnitude scientific notation for n,
// e.g. "1e2" for 100, "1e7" for 10 million.
func fmt1eN(n int) string {
return fmt.Sprintf("1e%d", int(math.Log10(float64(n))))
}

0 comments on commit 38f771d

Please sign in to comment.