[dev.fuzz] internal/fuzz: use scratch []byte for mutations
The mutator will now use a scratch []byte when mutating
[]byte and string types.
I ran the following target locally:
func FuzzBytesFromStringCmp(f *testing.F) {
f.Fuzz(func(t *testing.T, a, b string) {
bytes.Compare([]byte(a), []byte(b))
})
}
Before the change, execs were <400/sec:
=== FUZZ FuzzBytesFromStringCmp
fuzzing, elapsed: 3.0s, execs: 1090 (363/sec), workers: 8 ...
fuzzing, elapsed: 6.0s, execs: 2290 (382/sec), workers: 8 ...
fuzzing, elapsed: 9.0s, execs: 3491 (388/sec), workers: 8 ...
fuzzing, elapsed: 12.0s, execs: 4691 (391/sec), workers: 8 ...
fuzzing, elapsed: 15.0s, execs: 5869 (391/sec), workers: 8 ...
fuzzing, elapsed: 18.0s, execs: 7056 (392/sec), workers: 8 ...
After the change, the execs are ~6000/sec
=== FUZZ FuzzBytesFromStringCmp
fuzzing, elapsed: 3.0s, execs: 155129 (51687/sec), workers: 8 ...
fuzzing, elapsed: 6.0s, execs: 303710 (50606/sec), workers: 8 ...
fuzzing, elapsed: 9.0s, execs: 454314 (50470/sec), workers: 8 ...
fuzzing, elapsed: 12.0s, execs: 603212 (50262/sec), workers: 8 ...
fuzzing, elapsed: 15.0s, execs: 756165 (50401/sec), workers: 8 ...
fuzzing, elapsed: 18.0s, execs: 899293 (49955/sec), workers: 8 ...
Which is comparable to the same target with two []byte as input:
=== FUZZ FuzzBytesCmp
fuzzing, elapsed: 3.0s, execs: 152348 (50757/sec), workers: 8 ...
fuzzing, elapsed: 6.0s, execs: 314386 (52387/sec), workers: 8 ...
fuzzing, elapsed: 9.0s, execs: 487413 (54148/sec), workers: 8 ...
fuzzing, elapsed: 12.0s, execs: 646886 (53901/sec), workers: 8 ...
fuzzing, elapsed: 15.0s, execs: 814257 (54266/sec), workers: 8 ...
fuzzing, elapsed: 18.0s, execs: 983214 (54619/sec), workers: 8 ...
Benchark results:
name old time/op new time/op delta
MutatorBytes/1-8 7.70ms ± 3% 0.00ms ± 3% -99.99% (p=0.029 n=4+4)
MutatorBytes/10-8 7.88ms ± 2% 0.00ms ± 6% -99.99% (p=0.029 n=4+4)
MutatorBytes/100-8 7.87ms ± 1% 0.00ms ± 2% -99.99% (p=0.029 n=4+4)
MutatorBytes/1000-8 8.11ms ± 5% 0.00ms ± 2% -99.99% (p=0.029 n=4+4)
MutatorBytes/10000-8 8.11ms ± 4% 0.00ms ± 2% -99.99% (p=0.029 n=4+4)
MutatorBytes/100000-8 8.28ms ±10% 0.00ms ± 4% -99.96% (p=0.029 n=4+4)
MutatorString/1-8 7.89ms ± 5% 0.00ms ±17% -99.99% (p=0.029 n=4+4)
MutatorString/10-8 7.91ms ± 4% 0.00ms ± 7% -99.99% (p=0.029 n=4+4)
MutatorString/100-8 8.08ms ± 4% 0.00ms ± 7% -99.99% (p=0.029 n=4+4)
MutatorString/1000-8 8.11ms ± 6% 0.00ms ±11% -99.99% (p=0.029 n=4+4)
MutatorString/10000-8 8.04ms ± 7% 0.00ms ± 8% -99.98% (p=0.029 n=4+4)
MutatorString/100000-8 8.24ms ± 7% 0.01ms ±13% -99.82% (p=0.029 n=4+4)
Fixes #46543
Change-Id: I8b078ed3adc1bb6310c33afc49bb6cd78e7e976c
Reviewed-on: https://go-review.googlesource.com/c/go/+/324849
Trust: Katie Hockman <katie@golang.org>
Run-TryBot: Katie Hockman <katie@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
diff --git a/src/internal/fuzz/mutator.go b/src/internal/fuzz/mutator.go
index 2d7dbe6..9aa5678 100644
--- a/src/internal/fuzz/mutator.go
+++ b/src/internal/fuzz/mutator.go
@@ -13,7 +13,8 @@
)
type mutator struct {
- r mutatorRand
+ r mutatorRand
+ scratch []byte // scratch slice to avoid additional allocations
}
func newMutator() *mutator {
@@ -95,27 +96,34 @@
case byte: // uint8
vals[i] = byte(m.mutateUInt(uint64(v), math.MaxUint8))
case string:
- // TODO(jayconrod,katiehockman): Keep a []byte somewhere (maybe in
- // mutator) that we mutate repeatedly to avoid re-allocating the data
- // every time.
if len(v) > maxPerVal {
panic(fmt.Sprintf("cannot mutate bytes of length %d", len(v)))
}
- b := []byte(v)
- if cap(b) < maxPerVal {
- b = append(make([]byte, 0, maxPerVal), b...)
+ if cap(m.scratch) < maxPerVal {
+ m.scratch = append(make([]byte, 0, maxPerVal), v...)
+ } else {
+ m.scratch = m.scratch[:len(v)]
+ copy(m.scratch, v)
}
- m.mutateBytes(&b)
- vals[i] = string(b)
+ m.mutateBytes(&m.scratch)
+ var s string
+ shdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
+ bhdr := (*reflect.SliceHeader)(unsafe.Pointer(&m.scratch))
+ shdr.Data = bhdr.Data
+ shdr.Len = bhdr.Len
+ vals[i] = s
case []byte:
if len(v) > maxPerVal {
panic(fmt.Sprintf("cannot mutate bytes of length %d", len(v)))
}
- if cap(v) < maxPerVal {
- v = append(make([]byte, 0, maxPerVal), v...)
+ if cap(m.scratch) < maxPerVal {
+ m.scratch = append(make([]byte, 0, maxPerVal), v...)
+ } else {
+ m.scratch = m.scratch[:len(v)]
+ copy(m.scratch, v)
}
- m.mutateBytes(&v)
- vals[i] = v
+ m.mutateBytes(&m.scratch)
+ vals[i] = m.scratch
default:
panic(fmt.Sprintf("type not supported for mutating: %T", vals[i]))
}
diff --git a/src/internal/fuzz/mutator_test.go b/src/internal/fuzz/mutator_test.go
index 5fcfb27..ee2912d 100644
--- a/src/internal/fuzz/mutator_test.go
+++ b/src/internal/fuzz/mutator_test.go
@@ -15,6 +15,7 @@
origEnv := os.Getenv("GODEBUG")
defer func() { os.Setenv("GODEBUG", origEnv) }()
os.Setenv("GODEBUG", fmt.Sprintf("%s,fuzzseed=123", origEnv))
+ m := newMutator()
for _, size := range []int{
1,
@@ -24,7 +25,6 @@
10000,
100000,
} {
- size := size
b.Run(strconv.Itoa(size), func(b *testing.B) {
buf := make([]byte, size)
b.ResetTimer()
@@ -32,7 +32,7 @@
for i := 0; i < b.N; i++ {
// resize buffer to the correct shape and reset the PCG
buf = buf[0:size]
- m := newMutator()
+ m.r = newPcgRand()
m.mutate([]interface{}{buf}, workerSharedMemSize)
}
})
@@ -43,6 +43,7 @@
origEnv := os.Getenv("GODEBUG")
defer func() { os.Setenv("GODEBUG", origEnv) }()
os.Setenv("GODEBUG", fmt.Sprintf("%s,fuzzseed=123", origEnv))
+ m := newMutator()
for _, size := range []int{
1,
@@ -52,7 +53,6 @@
10000,
100000,
} {
- size := size
b.Run(strconv.Itoa(size), func(b *testing.B) {
buf := make([]byte, size)
b.ResetTimer()
@@ -60,7 +60,7 @@
for i := 0; i < b.N; i++ {
// resize buffer to the correct shape and reset the PCG
buf = buf[0:size]
- m := newMutator()
+ m.r = newPcgRand()
m.mutate([]interface{}{string(buf)}, workerSharedMemSize)
}
})
@@ -71,6 +71,7 @@
origEnv := os.Getenv("GODEBUG")
defer func() { os.Setenv("GODEBUG", origEnv) }()
os.Setenv("GODEBUG", fmt.Sprintf("%s,fuzzseed=123", origEnv))
+ m := newMutator()
types := []interface{}{
[]byte(""),
@@ -92,7 +93,7 @@
for _, t := range types {
b.Run(fmt.Sprintf("%T", t), func(b *testing.B) {
for i := 0; i < b.N; i++ {
- m := newMutator()
+ m.r = newPcgRand()
m.mutate([]interface{}{t}, workerSharedMemSize)
}
})
diff --git a/src/testing/fuzz.go b/src/testing/fuzz.go
index 55e5397..d62eb55 100644
--- a/src/testing/fuzz.go
+++ b/src/testing/fuzz.go
@@ -258,9 +258,18 @@
// Fuzz runs the fuzz function, ff, for fuzz testing. If ff fails for a set of
// arguments, those arguments will be added to the seed corpus.
//
+// ff must be a function with no return value whose first argument is *T and
+// whose remaining arguments are the types to be fuzzed.
+// For example:
+//
+// f.Fuzz(func(t *testing.T, b []byte, i int) { ... })
+//
+// This function should be fast, deterministic, and stateless.
+// None of the pointers to any input data should be retained between executions.
+//
// This is a terminal function which will terminate the currently running fuzz
-// target by calling runtime.Goexit. To run any code after this function, use
-// Cleanup.
+// target by calling runtime.Goexit.
+// To run any code after fuzzing stops, use (*F).Cleanup.
func (f *F) Fuzz(ff interface{}) {
if f.fuzzCalled {
panic("testing: F.Fuzz called more than once")