Description
Go version
go version devel go1.25-4f45b2b7e0 Tue Mar 4 03:10:17 2025 -0800 linux/arm64
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=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/jlapenko/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/jlapenko/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2889077743=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='arm64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/home/jlapenko/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/jlapenko/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/jlapenko/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/jlapenko/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/jlapenko/go/pkg/tool/linux_arm64'
GOVCS=''
GOVERSION='devel go1.25-4f45b2b7e0 Tue Mar 4 03:10:17 2025 -0800'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
Consider the following example (it is based on the function (*Resolver).resolveAddrList
from net/dial.go
):
// file zero_test.go
package zero_weight
import "testing"
type Addr interface {
Network() string
}
type UnixAddr struct {
Net string
}
type NotUnixAddr struct {
Net string
}
func (a *UnixAddr) Network() string {
return a.Net
}
func (a *NotUnixAddr) Network() string {
return a.Net
}
//go:noinline
func Unreachable(addr *UnixAddr, hint Addr) int {
if addr.Network() != hint.Network() { // redundant devirtualization?
return 1
}
return 0
}
//go:noinline
func Hot() int {
return 100
}
//go:noinline
func Action(param bool) {
if param {
addr := &UnixAddr{
Net: "net",
}
hint := &NotUnixAddr{
Net: "hint",
}
Unreachable(addr, hint)
} else {
Hot()
}
}
func BenchmarkZeroWeight(b *testing.B) {
for i := 0; i < b.N; i++ {
Action(false)
}
}
Build and collect a profile:
$ go test -c -a -o t.out ./zero_test.go
$ ./t.out -test.cpuprofile=default.pprof -test.count=3 -test.bench=.
What did you see happen?
As can be seen, Unreachable
is never called. Hence, it hasn't appeared in the profile:
$ go tool pprof -raw default.pprof
PeriodType: cpu nanoseconds
Period: 10000000
Time: 2025-03-04 12:53:46.38332888 +0000 UTC
Duration: 4.72
Samples:
samples/count cpu/nanoseconds
58 580000000: 1 2 3 4 5
92 920000000: 6 3 4 5
147 1470000000: 7 3 4 5
79 790000000: 8 4 5
73 730000000: 9 4 5
5 50000000: 10 3 4 5
Locations
1: 0x11c1e0 M=1 command-line-arguments.Hot /home/jlapenko/PGO_devirt/zero_weight/zero_test.go:37:0 s=36
2: 0x11c253 M=1 command-line-arguments.Action /home/jlapenko/PGO_devirt/zero_weight/zero_test.go:51:0 s=41
3: 0x11c2af M=1 command-line-arguments.BenchmarkZeroWeight /home/jlapenko/PGO_devirt/zero_weight/zero_test.go:57:0 s=55
4: 0xd74ef M=1 testing.(*B).runN /home/jlapenko/go/src/testing/benchmark.go:202:0 s=182
5: 0xd7fd3 M=1 testing.(*B).launch /home/jlapenko/go/src/testing/benchmark.go:333:0 s=307
6: 0x11c1f0 M=1 command-line-arguments.Action /home/jlapenko/PGO_devirt/zero_weight/zero_test.go:41:0 s=41
7: 0x11c258 M=1 command-line-arguments.Action /home/jlapenko/PGO_devirt/zero_weight/zero_test.go:53:0 s=41
8: 0x11c2a4 M=1 command-line-arguments.BenchmarkZeroWeight /home/jlapenko/PGO_devirt/zero_weight/zero_test.go:56:0 s=55
9: 0x11c2b8 M=1 command-line-arguments.BenchmarkZeroWeight /home/jlapenko/PGO_devirt/zero_weight/zero_test.go:56:0 s=55
10: 0x11c250 M=1 command-line-arguments.Action /home/jlapenko/PGO_devirt/zero_weight/zero_test.go:51:0 s=41
Mappings
1: 0x10000/0x127000/0x0 /home/jlapenko/PGO_devirt/zero_weight/t.out 9e696feb90352f1483c3df478ba7a8a1ee3e6aa0 [FN]
2: 0xffff94660000/0xffff94661000/0x0 [vdso]
However, the call hint.Network()
in the Unreachable
function has been devirtualized:
$ go test -c -a -pgo=default.pprof -gcflags='-m' -o t.pgo.out ./zero_test.go
# command-line-arguments [command-line-arguments.test]
./zero_test.go:29:35: PGO devirtualizing interface call hint.Network to (*UnixAddr).Network
./zero_test.go:19:6: can inline (*UnixAddr).Network
./zero_test.go:23:6: can inline (*NotUnixAddr).Network
./zero_test.go:55:6: can inline BenchmarkZeroWeight
./zero_test.go:29:17: inlining call to (*UnixAddr).Network
./zero_test.go:29:35: inlining call to (*UnixAddr).Network
...
The reason is the following. WeightedCG
contains all functions from a package being compiled. In our case (taken using -d=pgodebug=3
):
hot-cg before inline in dot format:
digraph G {
forcelabels=true;
"command-line-arguments.Unreachable" [color=black, style=solid, label="command-line-arguments.Unreachable"];
"command-line-arguments.Hot" [color=black, style=solid, label="command-line-arguments.Hot"];
"command-line-arguments.BenchmarkZeroWeight" [color=black, style=solid, label="command-line-arguments.BenchmarkZeroWeight"];
"command-line-arguments.Action" [color=black, style=solid, label="command-line-arguments.Action"];
"command-line-arguments.(*UnixAddr).Network" [color=black, style=solid, label="command-line-arguments.(*UnixAddr).Network"];
"command-line-arguments.(*NotUnixAddr).Network" [color=black, style=solid, label="command-line-arguments.(*NotUnixAddr).Network"];
edge [color=black, style=solid];
"command-line-arguments.Unreachable" -> "command-line-arguments.(*UnixAddr).Network" [label="0.00"];
edge [color=black, style=solid];
"command-line-arguments.Action" -> "command-line-arguments.Unreachable" [label="0.00"];
edge [color=black, style=solid];
"command-line-arguments.Action" -> "command-line-arguments.Hot" [label="12.78"];
edge [color=red, style=solid];
"command-line-arguments.BenchmarkZeroWeight" -> "command-line-arguments.Action" [label="55.51"];
}
There are nodes for both (*UnixAddr).Network
and (*NotUnixAddr).Network
functions. The edge Unreachable -> (*UnixAddr).Network
has a zero weight because there are no suitable samples in the profile.
PGO devirtualization considers the call hint.Network()
and tries to find the most appropriate candidate. For the Unreachable
node, it goes through OutEdges
with the same callsite offset. Note that we can't distinguish callsites addr.Network()
and hint.Network()
because they are placed on the same line. The edge to (*UnixAddr).Network
is the only one because there are no samples related to the Unreachable
function. This callee's AST != nil
, and the method receiver type implements the interface Addr
. So, hint.Network()
is devirtualized to (*UnixAddr).Network
, although this caller relates to addr.Network()
and the weight of the edge is 0.
I suppose we shouldn't devirtualize a call when a weight of its hottest edge is 0 even if its AST != nil
. Moreover, the devirtualized call will be inlined later (as can be seen in the debug output shown above), and this leads to unjustified increase of code size.
What did you expect to see?
The call hint.Network()
shouldn't be devirtualized.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status