Skip to content

net: BenchmarkTCP4OneShot produces bimodal results across repeated runs #80640

Description

@hxygmy

Go version

go version go1.24.6 linux/amd64

Output of go env in your module/workspace:

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/hxy/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/hxy/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2828283384=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/home/hxy/app/go1.24/src/go.mod'
GOMODCACHE='/home/hxy/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/hxy/go'
GOPRIVATE=''
GOPROXY='https://goproxy.cn,direct'
GOROOT='/home/hxy/app/go1.24'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/hxy/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/hxy/app/go1.24/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.24.6'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

I ran the standard-library TCP benchmarks directly, without an external benchmark
wrapper:

GOMAXPROCS=16 taskset -c 0-15 +  go test -run='^$' +  -bench='^BenchmarkTCP4(OneShot|Persistent)$' +  -benchtime=1s -count=10 net

What did you see happen?

BenchmarkTCP4OneShot split into two repeatable groups, around 9.4 microseconds
and 22-24 microseconds per operation:

BenchmarkTCP4OneShot-16  125900  22375 ns/op
BenchmarkTCP4OneShot-16  123795  23303 ns/op
BenchmarkTCP4OneShot-16  126745  22781 ns/op
BenchmarkTCP4OneShot-16  114078  24431 ns/op
BenchmarkTCP4OneShot-16  127173  23575 ns/op
BenchmarkTCP4OneShot-16  127147   9425 ns/op
BenchmarkTCP4OneShot-16  123948   9423 ns/op
BenchmarkTCP4OneShot-16  125728   9475 ns/op
BenchmarkTCP4OneShot-16  126067   9520 ns/op
BenchmarkTCP4OneShot-16  126033   9423 ns/op

In a longer benchmark-suite run on the same host, BenchmarkTCP4Persistent also
occasionally split according to the calibrated iteration count:

BenchmarkTCP4Persistent-16       100  10227236 ns/op
BenchmarkTCP4Persistent-16       100  10226456 ns/op
BenchmarkTCP4Persistent-16       100  10226708 ns/op
BenchmarkTCP4Persistent-16     10000    102811 ns/op
BenchmarkTCP4Persistent-16       100  10170395 ns/op
BenchmarkTCP4Persistent-16     10000    102558 ns/op

The Persistent behavior did not reproduce in the standalone ten-count command,
where it stayed near 1.16 microseconds per operation, so it may require additional
system load or a transient stall.

What did you expect to see?

Repeated measurements of the same sub-benchmark should remain in one performance
population, apart from normal scheduler and socket noise. If OneShot is intended
to include connection lifecycle and goroutine creation effects, it would still be
useful for the benchmark to keep its concurrency and operation definition
independent of the adaptive value of b.N.

Why the benchmark implementation may contribute

In src/net/tcpsock_test.go:

  • OneShot sets conns := b.N and starts one goroutine per connection, with a
    semaphore limiting active connections.
  • Persistent derives both conns and msgs from b.N.
  • Listener setup, connection creation, goroutine startup, synchronization and
    teardown are all inside the timed benchmark interval.
  • Read, write, and dial failures are logged with b.Log and terminate a worker,
    but do not necessarily fail the benchmark or correct the reported operation
    count.

This couples Go's adaptive benchmark calibration with goroutine creation, socket
lifecycle, and fixed setup/synchronization costs. A transient delay during
calibration can therefore change both b.N and the measured ns/op population.

Possible direction

Would it make sense to:

  1. Keep the worker/concurrency count fixed independently of b.N.
  2. Exclude listener and worker setup from the timed interval.
  3. For Persistent, establish a fixed connection pool before starting the timer
    and execute exactly b.N request/response operations.
  4. For OneShot, keep Dial/exchange/Close in the operation but reuse a fixed worker
    pool instead of creating O(b.N) goroutines.
  5. Treat connection/read/write failures as a failed benchmark, or separately
    report completed operations.

I can prepare a change and test it across amd64 and arm64 if this direction matches
the intended semantics of these benchmarks.

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.Performance

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions