Skip to content
This repository has been archived by the owner on Jan 10, 2024. It is now read-only.

Optimize randBool() and randString() #41

Merged
merged 4 commits into from
Jun 4, 2020
Merged

Conversation

jake-ciolek
Copy link
Contributor

I love this library, small but gets the job done! :)

I have tinkered a bit with the functions to generate bools and strings and made them a bit faster. Hopefully it will save someone some clocks when running tests.

The code speeds up bool generation 3-fold. I've ran some benchmarks on GCP VM's and my local MacBook:

My MacBook Pro:

BenchmarkRandBool-8 75312272 14.5 ns/op
BenchmarkRandBoolNew-8 235223112 5.06 ns/op
BenchmarkRandStringStock-8 1439635 841 ns/op
BenchmarkRandStringBuilder-8 1541736 775 ns/op

8 core GCP E2:

BenchmarkRandBool-8 70003590 16.6 ns/op
BenchmarkRandBoolNew-8 236062108 5.08 ns/op
BenchmarkRandStringStock-8 1370116 874 ns/op
BenchmarkRandStringBuilder-8 1484086 807 ns/op

8 core GCP N2 (Intel Cascade Lake):

BenchmarkRandBool-8 91276062 13.1 ns/op
BenchmarkRandBoolNew-8 301435986 3.98 ns/op
BenchmarkRandStringStock-8 1743852 688 ns/op
BenchmarkRandStringBuilder-8 1883102 638 ns/op

8 core GCP N1 (Intel Skylake):

BenchmarkRandBool-8 71896552 16.5 ns/op
BenchmarkRandBoolNew-8 239149131 5.03 ns/op
BenchmarkRandStringStock-8 1375860 873 ns/op
BenchmarkRandStringBuilder-8 1484378 808 ns/op

randString() will be 5-7% due to less data being moved around (thanks to strings.Builder)

randString() memory usage change:

BenchmarkRandStringStock-8 1543179 772 ns/op 71 B/op 1 allocs/op
BenchmarkRandStringBuilder-8 1639436 735 ns/op 48 B/op 1 allocs/op

  Previous time: 15.175 ns/op
  New time:      4.78 ns/op
  30% less memory used and a 5 to 7% higher throughput.
@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed (or fixed any issues), please reply here with @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

@jake-ciolek
Copy link
Contributor Author

@googlebot I signed it!

@googlebot
Copy link

CLAs look good, thanks!

ℹ️ Googlers: Go here for more info.

@googlebot googlebot added cla: yes and removed cla: no labels Jun 4, 2020
return true
}
return false
return r.Int31()&(1<<30) == 0
Copy link
Contributor

Choose a reason for hiding this comment

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

Why bit 31 and not some other bit?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to pick one and I believe there was no better choice :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, they should all be the same. I just wasn't sure if the change from bit 0 was significant for the speed up--it shouldn't be, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From my benchmarks, on my local machine I've found this bit to be faster from bit 0 by 1-2% - why, I don't know - probably some CPU arch black magic.

Copy link
Contributor

Choose a reason for hiding this comment

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

hm... you can use https://godoc.org/golang.org/x/tools/cmd/benchcmp to tell if it's significant or not. (not shown in the help: you can put multiple runs in both the old and new file and it will give you the std dev so you can tell if it's significant or not)

@lavalamp
Copy link
Contributor

lavalamp commented Jun 4, 2020

Thanks! Can you add your benchmark, too?

@jake-ciolek
Copy link
Contributor Author

Sure!

import (
	"math/rand"

	"testing"
	"strings"
)

func randBoolOld(r *rand.Rand) bool {
	if r.Int()&1 == 1 {
		return true
	}
	return false
}

func randBoolNew(r *rand.Rand) bool {
	return r.Int31()&(1<<30) == 0
}

func randStringOld(r *rand.Rand) string {
	n := r.Intn(20)
	runes := make([]rune, n)
	for i := range runes {
		runes[i] = unicodeRanges[r.Intn(len(unicodeRanges))].choose(r)
	}
	return string(runes)
}

func randStringBuilder(r *rand.Rand) string {
	n := r.Intn(20)
	sb := strings.Builder{}
        sb.Grow(n)
	for i := 0 ;i<n; i++ {
                sb.WriteRune(unicodeRanges[r.Intn(len(unicodeRanges))].choose(r))
	}
	
	return sb.String()
}

type int63nPicker interface {
	Int63n(int64) int64
}

type charRange struct {
	first, last rune
}

// choose returns a random unicode character from the given range, using the
// given randomness source.
func (cr charRange) choose(r int63nPicker) rune {
	count := int64(cr.last - cr.first + 1)
	return cr.first + rune(r.Int63n(count))
}

var unicodeRanges = []charRange{
	{' ', '~'},           // ASCII characters
	{'\u00a0', '\u02af'}, // Multi-byte encoded characters
	{'\u4e00', '\u9fff'}, // Common CJK (even longer encodings)
}

func BenchmarkRandBool(b *testing.B) {
	rs := rand.New(rand.NewSource(123))
	for i := 0; i < b.N; i++ {
		randBoolOld(rs)
    }
}

func BenchmarkRandBoolNew(b *testing.B) {
	rs := rand.New(rand.NewSource(123))
	for i := 0; i < b.N; i++ {
		randBoolNew(rs)
    }
}

func BenchmarkRandStringStock(b *testing.B) {
	rs := rand.New(rand.NewSource(123))
	for i := 0; i < b.N; i++ {
		randStringOld(rs)
    }
}

func BenchmarkRandStringBuilder(b *testing.B) {
	rs := rand.New(rand.NewSource(123))
	for i := 0; i < b.N; i++ {
		randStringBuilder(rs)
    }
}

@lavalamp
Copy link
Contributor

lavalamp commented Jun 4, 2020

Can you add the benchmarks to the _test.go file? (just one for Bool and one for String; no need for old/new--we can just track that over time)

@jake-ciolek
Copy link
Contributor Author

Added benchmarks :)

fuzz_test.go Outdated
rs := rand.New(rand.NewSource(123))

for i := 0; i < b.N; i++ {
randBool(rs)
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks good-- can you run gofmt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done 😃

for i := range runes {
runes[i] = unicodeRanges[r.Intn(len(unicodeRanges))].choose(r)
sb := strings.Builder{}
sb.Grow(n)
Copy link
Contributor

@lavalamp lavalamp Jun 4, 2020

Choose a reason for hiding this comment

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

I bet n*2 or n*3 would avoid an extra allocation or two.

@lavalamp
Copy link
Contributor

lavalamp commented Jun 4, 2020

LGTM thanks!

@lavalamp lavalamp merged commit c04b05f into google:master Jun 4, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants