[dev.fuzz] internal/fuzz: reduce allocation in the mutator

When mutating a byte slice, mutate in place, and only allocate once if
the slice's capacity is less than the maximum size.

mutateBytes already should not allocate; we check a post-condition
that the slice's data pointer does not change.

This speeds up the mutator from 4 ms per value to 200-600 ns. For
example:

    goos: darwin
    goarch: amd64
    pkg: internal/fuzz
    cpu: Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
    BenchmarkMutatorBytes/1-8                5908735               275.3 ns/op
    BenchmarkMutatorBytes/10-8               5198473               282.0 ns/op
    BenchmarkMutatorBytes/100-8              4304750               233.9 ns/op
    BenchmarkMutatorBytes/1000-8             4623988               295.2 ns/op
    BenchmarkMutatorBytes/10000-8            4252104               458.5 ns/op
    BenchmarkMutatorBytes/100000-8           1236751               950.8 ns/op
    PASS
    ok      internal/fuzz   12.993s

Change-Id: I4bf2a04be6c648ef440af2c62bf0ffa3d310172c
Reviewed-on: https://go-review.googlesource.com/c/go/+/306675
Trust: Jay Conrod <jayconrod@google.com>
Trust: Katie Hockman <katie@golang.org>
Run-TryBot: Jay Conrod <jayconrod@google.com>
Reviewed-by: Katie Hockman <katie@golang.org>
This commit is contained in:
Jay Conrod 2021-04-01 17:51:18 -04:00
parent 9d4d5ee66d
commit b975d0baa0
4 changed files with 42 additions and 10 deletions

View File

@ -51,7 +51,6 @@ func CoordinateFuzzing(ctx context.Context, parallel int, seed []CorpusEntry, ty
parallel = runtime.GOMAXPROCS(0)
}
sharedMemSize := 100 << 20 // 100 MB
// Make sure all of the seed corpus has marshalled data.
for i := range seed {
if seed[i].Data == nil {
@ -85,7 +84,7 @@ func CoordinateFuzzing(ctx context.Context, parallel int, seed []CorpusEntry, ty
errC := make(chan error)
newWorker := func() (*worker, error) {
mem, err := sharedMemTempFile(sharedMemSize)
mem, err := sharedMemTempFile(workerSharedMemSize)
if err != nil {
return nil, err
}

View File

@ -51,8 +51,7 @@ func min(a, b int) int {
}
// mutate performs several mutations on the provided values.
func (m *mutator) mutate(vals []interface{}, maxBytes int) []interface{} {
// TODO(jayconrod,katiehockman): use as few allocations as possible
func (m *mutator) mutate(vals []interface{}, maxBytes int) {
// TODO(katiehockman): pull some of these functions into helper methods and
// test that each case is working as expected.
// TODO(katiehockman): perform more types of mutations.
@ -71,11 +70,11 @@ func (m *mutator) mutate(vals []interface{}, maxBytes int) []interface{} {
if len(v) > maxPerVal {
panic(fmt.Sprintf("cannot mutate bytes of length %d", len(v)))
}
b := make([]byte, 0, maxPerVal)
b = append(b, v...)
m.mutateBytes(&b)
vals[i] = b
return vals
if cap(v) < maxPerVal {
v = append(make([]byte, 0, maxPerVal), v...)
}
m.mutateBytes(&v)
vals[i] = v
default:
panic(fmt.Sprintf("type not supported for mutating: %T", vals[i]))
}

View File

@ -0,0 +1,30 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package fuzz
import (
"strconv"
"testing"
)
func BenchmarkMutatorBytes(b *testing.B) {
for _, size := range []int{
1,
10,
100,
1000,
10000,
100000,
} {
size := size
b.Run(strconv.Itoa(size), func(b *testing.B) {
vals := []interface{}{make([]byte, size)}
m := newMutator()
for i := 0; i < b.N; i++ {
m.mutate(vals, workerSharedMemSize)
}
})
}
}

View File

@ -31,6 +31,10 @@ const (
// This distinguishes internal errors from uncontrolled panics and other crashes.
// Keep in sync with internal/fuzz.workerExitCode.
workerExitCode = 70
// workerSharedMemSize is the maximum size of the shared memory file used to
// communicate with workers. This limits the size of fuzz inputs.
workerSharedMemSize = 100 << 20 // 100 MB
)
// worker manages a worker process running a test binary. The worker object
@ -508,7 +512,7 @@ func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) fuzzResponse {
// real heuristic once we have one.
return fuzzResponse{Interesting: true}
default:
vals = ws.m.mutate(vals, cap(mem.valueRef()))
ws.m.mutate(vals, cap(mem.valueRef()))
writeToMem(vals, mem)
if err := ws.fuzzFn(CorpusEntry{Values: vals}); err != nil {
if minErr := ws.minimize(ctx, vals, mem); minErr != nil {