-
Notifications
You must be signed in to change notification settings - Fork 18.5k
Description
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='0'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/john/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/john/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -fno-caret-diagnostics -Qunused-arguments -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build3904074702=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/home/john/tmp/gogenericopt/go.mod'
GOMODCACHE='/home/john/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/john/go:/home/john/src/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/john/go-1.24.6'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/john/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/john/go-1.24.6/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.24.6'
GOWORK=''
PKG_CONFIG='pkg-config'What did you do?
Given the following code:
package gogenericopt
import (
"testing"
)
type Adder[T any] interface {
Add(a, b T) T
}
type Float interface {
~float32 | ~float64
}
type FloatAdder[T Float] struct{}
func (FloatAdder[T]) Add(a, b T) T { return a + b }
//go:noinline
func SumGenericAdder[A Adder[T], T any](a A, s []T) T {
var result T
for _, e := range s {
result = a.Add(result, e)
}
return result
}
//go:noinline
func SumFloatAdder[T Float](a FloatAdder[T], s []T) T {
var result T
for _, e := range s {
result = a.Add(result, e)
}
return result
}
func BenchmarkSumGenericAdder_float32(b *testing.B) {
s := genSlice[float32]()
for b.Loop() {
SumGenericAdder(FloatAdder[float32]{}, s)
}
}
func BenchmarkSumFloatAdder_float32(b *testing.B) {
s := genSlice[float32]()
for b.Loop() {
SumFloatAdder(FloatAdder[float32]{}, s)
}
}
func genSlice[T Float]() []T {
const size = 128
s := make([]T, size)
for i := range s {
s[i] = T(1.0 / size)
}
return s
}and running go test -bench=. ...
What did you see happen?
goos: linux
goarch: amd64
pkg: example.com/gogenericopt
cpu: Intel(R) Core(TM) Ultra 7 155U
BenchmarkSumGenericAdder_float32-14 9139267 133.5 ns/op
BenchmarkSumFloatAdder_float32-14 28473896 41.75 ns/op
PASS
ok example.com/gogenericopt 2.414s
What did you expect to see?
I would have expected similar benchmark results for the two functions, since in both cases, the type of adder is known at compile time to be
FloatAdder[float32].
It seems that in the case where the adder's type is a
generic parameter constrained by Adder[T], the compiler is not trying to
inline the Add method. Yet, if it's taken as a FloatAdder[T], the compiler
does inline the method call.
A type parameter T is involved in both cases; the difference seems to be
whether the adder type is constrained by an interface or is an instantiation
of a generic type with the type parameter T.
Is this just a missed case in the optimizer, or is there some technical
difficulty supporting the first case?
From go test -bench=. -gcflags=-m:
# example.com/gogenericopt [example.com/gogenericopt.test]
./gogenericopt_test.go:51:6: can inline genSlice[go.shape.float32]
./gogenericopt_test.go:17:6: can inline FloatAdder[go.shape.float32].Add
./gogenericopt_test.go:17:6: can inline FloatAdder[float32].Add
./gogenericopt_test.go:51:6: can inline genSlice[float32]
./gogenericopt_test.go:39:2: skip inlining within testing.B.loop for for loop
./gogenericopt_test.go:46:2: skip inlining within testing.B.loop for for loop
./gogenericopt_test.go:38:24: inlining call to genSlice[go.shape.float32]
./gogenericopt_test.go:39:12: inlining call to testing.(*B).Loop
./gogenericopt_test.go:32:17: inlining call to FloatAdder[go.shape.float32].Add
./gogenericopt_test.go:45:24: inlining call to genSlice[go.shape.float32]
./gogenericopt_test.go:46:12: inlining call to testing.(*B).Loop
./gogenericopt_test.go:17:6: inlining call to FloatAdder[go.shape.float32].Add
./gogenericopt_test.go:51:6: inlining call to genSlice[go.shape.float32]
<autogenerated>:1: inlining call to FloatAdder[float32].Add
<autogenerated>:1: inlining call to FloatAdder[go.shape.float32].Add
<autogenerated>:1: inlining call to FloatAdder[go.shape.float32].Add
./gogenericopt_test.go:37:39: leaking param: b
./gogenericopt_test.go:38:24: make([]go.shape.float32, 128) does not escape
./gogenericopt_test.go:44:37: leaking param: b
./gogenericopt_test.go:45:24: make([]go.shape.float32, 128) does not escape
./gogenericopt_test.go:53:11: make([]go.shape.float32, 128) escapes to heap
./gogenericopt_test.go:51:6: make([]go.shape.float32, 128) escapes to heap
# example.com/gogenericopt.test
_testmain.go:39:6: can inline init.0
<autogenerated>:1: inlining call to reflect.flag.kind
<autogenerated>:1: inlining call to reflect.flag.kind
<autogenerated>:1: inlining call to reflect.flag.mustBe
<autogenerated>:1: inlining call to reflect.flag.kind
<autogenerated>:1: inlining call to reflect.flag.mustBe
<autogenerated>:1: inlining call to reflect.flag.kind
<autogenerated>:1: inlining call to reflect.flag.mustBeAssignable
<autogenerated>:1: inlining call to reflect.flag.mustBeAssignable
<autogenerated>:1: inlining call to reflect.flag.mustBeExported
<autogenerated>:1: inlining call to reflect.flag.mustBeExported
<autogenerated>:1: inlining call to reflect.flag.ro
<autogenerated>:1: inlining call to reflect.flag.ro
_testmain.go:45:42: testdeps.TestDeps{} escapes to heap
<autogenerated>:1: &reflect.ValueError{...} escapes to heap
<autogenerated>:1: &reflect.ValueError{...} escapes to heap
The assembly from -gcflags=-S also makes it clear that the Add call is not
inlined in the SumGenericAdder instantiation but it is inlined in the
SumFloatAdder instantiation. I can attach it if it would be useful.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status