Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/compile: long symbol names for instantiated generics => large object files (though not executables) #50438

Open
csgura opened this issue Jan 5, 2022 · 16 comments
Assignees
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. generics Issue is related to generics NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Milestone

Comments

@csgura
Copy link

csgura commented Jan 5, 2022

What version of Go are you using (go version)?

$ go version
go version devel go1.18-f154f8b Tue Jan 4 22:27:20 2022 +0000 darwin/amd64

Does this issue reproduce with the latest release?

Yes

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/gura/Library/Caches/go-build"
GOENV="/Users/gura/Library/Application Support/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOINSECURE=""
GOMODCACHE="/Users/gura/go/pkg/mod"
GONOPROXY="*.uangel.com"
GONOSUMDB="*.uangel.com"
GOOS="darwin"
GOPATH="/Users/gura/go"
GOPRIVATE="*.uangel.com"
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/Users/gura/sdk/gotip"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/Users/gura/sdk/gotip/pkg/tool/darwin_amd64"
GOVCS=""
GOVERSION="devel go1.18-f154f8b Tue Jan 4 22:27:20 2022 +0000"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD="/Users/gura/test/fp/go.mod"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/y1/bngm83dj5_5dcsgh9yrwvpm00000gp/T/go-build228120003=/tmp/go-build -gno-record-gcc-switches -fno-common"

What did you do?

$ git clone -b build-cache https://github.com/csgura/fp.git
$ cd fp
$ gotip clean -cache
$ du -hs ~/Library/Caches/go-build/
8.0K	/Users/gura/Library/Caches/go-build/

$ gotip test ./...

$ du -hs ~/Library/Caches/go-build/
1.4G	/Users/gura/Library/Caches/go-build/

What did you expect to see?

A build cache directory of a reasonable size ,
or
Warning about incorrectly used generic type.

What did you see instead?

$ du -hs ~/Library/Caches/go-build/
1.4G	/Users/gura/Library/Caches/go-build/

Please excuse my poor English.

It seems to be related to this issue as well. #50204

I have written some algebraic data types to test the generic of Go 1.18.
( https://github.com/csgura/fp.git )
After running the tests, I noticed that the build cache was using a very large amount of disk.
I guess the cause lies in the some recursive type ( HList and curried Func ) and the interface type that uses the type parameter.

When I modified the code so that a generic interface type does not return other generic interface type,
The size of the build cache has been significantly reduced.
This fix is applied in the master branch.

$ git checkout master
$ gotip clean -cache
$ gotip test ./...
$ du -hs ~/Library/Caches/go-build/
176M	/Users/gura/Library/Caches/go-build/

It will not become a problem right now, but I think it will become a big problem as more and more projects use generics.

@rsc
Copy link
Contributor

rsc commented Jan 5, 2022

It would help if you could provide a small single source file that produces a large output using

go tool compile -o x.a x.go

@csgura
Copy link
Author

csgura commented Jan 6, 2022

Here is a single file of about 1500 lines.

https://gotipplay.golang.org/p/kMBLHxigsDo

$ gotip clean -cache
$ gotip build
$ gotip tool compile -o x.a main.go
$ du -hs ~/Library/Caches/go-build/
105M    /Users/gura/Library/Caches/go-build/

$ ls -al
total 142016
drwxr-xr-x   6 gura  staff       192 Jan  6 11:34 .
drwxr-xr-x  15 gura  staff       480 Jan  6 10:15 ..
-rwxr-xr-x   1 gura  staff   2501872 Jan  6 11:34 main
-rw-r--r--   1 gura  staff        27 Jan  6 10:16 go.mod
-rw-r--r--   1 gura  staff     26695 Jan  6 11:31 main.go
-rw-r--r--   1 gura  staff  70173912 Jan  6 11:34 x.a

@ianlancetaylor ianlancetaylor changed the title cmd/compile: Size of 'Caches/go-build' directory is too large. cmd/compile: size of 'Caches/go-build' directory is too large Jan 6, 2022
@ianlancetaylor ianlancetaylor changed the title cmd/compile: size of 'Caches/go-build' directory is too large cmd/compile: object file much larger than final executable Jan 6, 2022
@ianlancetaylor
Copy link
Member

With the test case above, go tool compile foo.go produces a file that is 70173802 bytes. go tool link foo.o produces a file that is 2400985 bytes. So the final executable is about 3.5% of the size of the object file.

The largest symbol in the object file is

type..namedata.*main.Tuple2[go.shape.interface { Concat(main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; Drop(int) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; DropWhile(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; Duplicate() main.Tuple2[main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0],main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]]; Exists(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) bool; Filter(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; FilterNot(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; Find(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) main.Option[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; ForAll(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) bool; Foreach(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0)); HasNext() bool; IsEmpty() bool; MakeString(string) string; Map(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) interface {}) main.Iterator[interface {}]; Next() go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0; NextOption() main.Option[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; NonEmpty() bool; Partition(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) main.Tuple2[main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0],main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]]; Reduce(main.Monoid[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]) go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0; Span(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) main.Tuple2[main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0],main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]]; Take(int) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; TakeWhile(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; TapEach(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0)) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; ToList() main.List[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; ToSeq() main.Seq[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0] }_0,go.shape.interface { Concat(main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; Drop(int) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; DropWhile(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; Duplicate() main.Tuple2[main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0],main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]]; Exists(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) bool; Filter(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; FilterNot(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; Find(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) main.Option[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; ForAll(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) bool; Foreach(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0)); HasNext() bool; IsEmpty() bool; MakeString(string) string; Map(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) interface {}) main.Iterator[interface {}]; Next() go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0; NextOption() main.Option[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; NonEmpty() bool; Partition(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) main.Tuple2[main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0],main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]]; Reduce(main.Monoid[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]) go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0; Span(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) main.Tuple2[main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0],main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]]; Take(int) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; TakeWhile(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0) bool) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; TapEach(func(go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0)) main.Iterator[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; ToList() main.List[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0]; ToSeq() main.Seq[go.shape.interface { Failed() main.Try[error]; Foreach(func(int)); Get() int; IsFailure() bool; IsSuccess() bool; Iterator() main.Iterator[int]; Or(func() main.Try[int]) main.Try[int]; OrElse(int) int; OrElseGet(func() int) int; Recover(func(error) int) main.Try[int]; RecoverWith(func(error) main.Try[int]) main.Try[int]; String() string; ToOption() main.Option[int]; ToSeq() main.Seq[int]; Unapply() (int, error) }_0] }_1].

It's 30111 bytes (according to go tool nm). It does not appear in the final executable. In fact, none of the 100 largest symbols in the object file appear in the executable.

This may be working as expected, but CC @randall77 @danscales in case it is not.

@csgura
Copy link
Author

csgura commented Jan 6, 2022

https://gotipplay.golang.org/p/8WZz12Ay6vz

It is almost the same code, but the Option, Try , HCons and Iterator types are changed to a struct type.

drwxr-xr-x  5 gura  staff       160 Jan  6 14:46 .
drwxr-xr-x  7 gura  staff       224 Jan  6 14:07 ..
-rwxr-xr-x  1 gura  staff   2114816 Jan  6 14:42 opti
-rw-r--r--  1 gura  staff     23637 Jan  6 14:42 opti.go
-rw-r--r--  1 gura  staff  17436020 Jan  6 14:46 x.a

The object file size has been reduced to 17436020.

It seems that object files are created inefficiently when generic interfaces refer to each other.
I know that it works as expected,
but wouldn't it be better to output a warning message when an object file is created inefficiently like that?

Because I think It would be better not to use type parameters with interface types in the production to reduce compile time.

@danscales
Copy link
Contributor

As Ian's symbol example shows, the symbols can be quite large for the names of instantiated functions/methods, when the type arguments are instantiated types that are nested and the descriptions of some of the underlying types (e.g. interfaces) are large. For the name of a shape type (the proxy type standing for all the types that a particular instantiation will handle), we use the standard printing (via LinkString) of the shared underlying type (e.g go.shape.int64_0 or go.shape.interface { M(); String() string }_0 or go.shape.struct { p.x int8; p.y float64 }_0. Using the name of the underlying type is easiest so that we have a unique name across packages. For Go 1.19, we could try to use shorter, unique, but memo-ized names, but then we would probably need extra coordination across compilations (using the build cache?). Or, if a shape name gets really large (> 50 characters), maybe we could replace it with an MD5 hash?

@ianlancetaylor
Copy link
Member

wouldn't it be better to output a warning message when an object file is created inefficiently like that?

No. This is something for a release note, not a compiler warning. Everything works correctly, it just takes more space than expected. The compiler never issues warnings anyhow.

@cagedmantis cagedmantis added NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. generics Issue is related to generics labels Jan 7, 2022
@cagedmantis cagedmantis added this to the Go1.18 milestone Jan 7, 2022
@akutz
Copy link

akutz commented Jan 25, 2022

FWIW, I've written a few tests to show that while package archives are definitely larger (usually 2x), the resulting binary executable shows no real difference -- https://github.com/akutz/go-generics-the-hard-way/blob/main/06-benchmarks/03-file-sizes.md.

@ianlancetaylor
Copy link
Member

@danscales This is in the 1.18 milestone; time to move to 1.19? Thanks.

@danscales
Copy link
Contributor

Yes, I'll move to 1.19. Thanks!

@danscales danscales modified the milestones: Go1.18, Go1.19 Jan 29, 2022
@thanm thanm modified the milestones: Go1.19, Go1.20 May 17, 2022
@gopherbot gopherbot added the compiler/runtime Issues related to the Go compiler and/or runtime. label Jul 13, 2022
@mknyszek mknyszek moved this to Triage Backlog in Go Compiler / Runtime Jul 15, 2022
@mknyszek mknyszek moved this from Triage Backlog to Todo in Go Compiler / Runtime Nov 23, 2022
@mknyszek mknyszek modified the milestones: Go1.20, Backlog Nov 30, 2022
@adonovan adonovan changed the title cmd/compile: object file much larger than final executable cmd/compile: long symbol names for instantiated generics => large object files (though not executables) Jan 11, 2024
@arvidfm
Copy link

arvidfm commented Aug 18, 2024

I spent some time digging into this as we've been having significant issues with the build cache growing much too large, even with the type hashing introduced with Go 1.22 to fix #65030. I've created a MWE based on my findings which generates a 50 MB object file from ~130 lines of code: https://github.com/arvidfm/go-cache-bloat.

The large object files seem to be a result of several factors compounding to make the issue even worse:

  1. The long type names mentioned in this issue
  2. The type names being repeated multiple times throughout the object file: In the string table, debug symbols, SRODATA symbols, etc, and additionally as prefixes for every instantiated method for the type
  3. All methods of generic types always being instantiated even if not used
  4. Method instantiations of generic types being duplicated in all packages that (indirectly) reference a certain instantiation of the type (see below for an example)
    • This in particular I suspect is the main culprit behind the poor compilation times we're seeing, as it appears to result in O(n²) compilation times for long dependency chains with respect to the number of packages in the chain
  5. Heavy inlining causing functions to grow deceptively large, which is particularly problematic for methods that are instantiated a lot

For us this adds up to several ~300 MB package archives being generated as part of the compilation, totalling ~5 GB for a single compilation from a clean cache. Making a single change to a central package results in most other packages being recompiled, resulting in another 5 GB, which quickly adds up.

I suspect (5) is unavoidable (and if anything desirable performance wise were it not for the other issues), and (3) is probably a requirement for interfaces to work. Hopefully something can be done about (1), (2) and (4), however. Maybe someone more familiar with the compiler internals could chime in to note which of these might be easiest to tackle. (It seems to me like it should be possible to avoid instantiating a type if the same instance is already present in one of the imported package archives.)

To better explain what I mean by (4), consider the following example, where the object files for both the main and a packages will contain identical instantiations of GenericType[int].GenericFunc (as seen with go tool compile -S), even though main doesn't reference GenericType[int] directly:

// a/a.go
package a

type GenericType[T any] struct{}

func (GenericType[T]) GenericFunc() {}

type A struct{}

func (A) AFunc(GenericType[int]) {}

// main.go
package main

import "example.com/cache/a"

type B struct {
	A a.A
}

func main() {}

This compounds for long dependency chains like main -> a.A -> b.B -> c.C -> d.D where each package will contain instantiations for all methods of the generic types used by all its (indirect) dependencies.

To get a better idea of specifically where the bloat is coming from, I ran a test on a real-world codebase that makes heavy use of generics. I created a new package containing a single file which calls a single generic function, referencing a type from another package that in turn results in a chain of generic type instantiations. The package looked something like this:

package mytest

import (
	"github.com/blah/blah/core"
	"github.com/blah/blah/users"
)

func doThing() {
	core.DoAction[users.User]()
}

Running go build -o mytest.a on this resulted in a 50 MB archive. I added some code to go tool compile to collect statistics on what parts of the object file were taking the most space and ended up with the following (line numbers are approximate as the files contain modifications):

image

  • 18 MB: string table
  • 13.6 MB: data section
    • 4.3 MB: SDWARFFCN symbols
      • ~3.4 MB of which appear to be strings already present in the string table
    • 3.1 MB: SRODATA symbols
      • ~1.4 MB of which appear to be strings already present in the string table
      • 1.1 MB of which are NULL bytes
    • 2.4 MB: STEXT symbols
    • 2.1 MB: SDWARFLOC symbols
      • 1.4 MB of which are NULL bytes
  • 8.1 MB: relocs
  • 2.8 MB: hashed symbol definitions
  • 2.5 MB: reloc, symbol info and data indexes
  • 2.1 MB: aux symbol info
  • 2.1 MB: hashes
  • 1.4 MB: non-package symbol definitions and references

I suspect the vast, vast majority of data here is from duplicate method instantiations already present in other package archives. Each individual entry in the reloc section is small, but there are just so many of them that they add up, presumably due to the duplicate method instantiations.

As for how to best mitigate the issue, this is the best I've managed to come up with in terms of practical advice for the end user:

  • Reduce the number of duplicate method instantiations
    • Avoid long dependency chains where each package instantiates different generic types
    • Prefer generic functions over methods on generic types
    • Write fewer longer methods instead of many small methods
      • Keeps the number of duplicated symbol names down
    • Use pointer receivers instead of value receivers
      • For methods with value receivers, Go will also generate a corresponding method with a pointer receiver, along with a corresponding symbol name, effectively doubling the symbol name length
    • Avoid large type switches that result in a lot of unnecessary instantiations, e.g.:
    switch any(p).(type) {
    case Pair[A, int], Pair[A, int64], Pair[A, int32], Pair[A, int16], Pair[A, int8]:
    case Pair[A, uint], Pair[A, uint8], Pair[A, uint16], Pair[A, uint32], Pair[A, uint64], Pair[A, uintptr]:
    case Pair[A, float32], Pair[A, float64]:
    }
  • Keep generic functions small if they need to be instantiated a lot
    • Use go tool nm -size -sort size to find potentially problematic functions
    • Move as much logic as possible to non-generic functions
    • Use //go:noinline as needed to prevent inlining from blowing up the size of your generic functions
  • Keep symbol names small
    • Avoid using struct types as type parameters
    • If you have to use struct types as type parameters, make them either very small, or large enough to get hashed
    • Remember that field tags are included in the symbol name

Of course, most of this is absolutely horrible advice from a maintainability, readability and runtime performance perspective, and not particularly helpful if you've already implemented a certain architecture and can't afford to break compatibility, so I do hope that the issue can be fixed on the compiler side.

@arvidfm
Copy link

arvidfm commented Aug 18, 2024

Turns out that there is an open issue for the duplicate generic instantiation (or at least a specific case of it): #56718

@timothy-king
Copy link
Contributor

#50438 (comment) thank you for the breakdown. I am not sure when I will have cycles to investigate this, but I would like to so I will optimistically assign this to myself.

@timothy-king
Copy link
Contributor

I started looking at https://github.com/arvidfm/go-cache-bloat . My initial observation is that it is requesting a very large number of types be instantiated, and those types have a large number of methods. I will keep looking, but TBH the number of types instantiated seems like the problem that should be solved. Compressing the size of the strings would help, but it is a secondary symptom.

FWIW switching Func11 from a method to a function, dramatically helps. c.o goes from 60MB to 2.5MB with that change. This cuts down the number of types generated really dramatically.

(If you have a more realistic example than go-cache-bloat that would help a lot.)

@arvidfm
Copy link

arvidfm commented Dec 6, 2024

@timothy-king The code in that repo is a contrived version (intentionally crafted to result in as much blowup as possible in as few lines of code as possible) of a pattern we use in practice in our codebase for a sort of typesafe query AST builder. Say we have a number of different models containing data (think e.g. tables in SQL), and we want to represent typed expressions that manipulate data from those models. You can't combine expressions evaluated against different models, which we want to encode at the type level.

So we might represent this like:

type Expression[Model, Type any] struct {
    rawExpr string
}

To make building these expressions more ergonomic and readable, we have a lot of small methods for composing expressions, e.g.:

func (e Expression[Model, Type]) Add(expr Expression[Model, Type]) Expression[Model, Type] {
    return Expression[Model, Type]{rawExpr: fmt.Sprintf("%s + %s", e.rawExpr, expr.rawExpr)}
}

func (e Expression[Model, Type]) Eq(value Type) Expression[Model, bool] {
    return Expression[Model, bool]{rawExpr: fmt.Sprintf("%s == %v", e.rawExpr, value)}
}

func (e Expression[Model, Type]) IsNull() Expression[Model, bool] {
    // ...
}

etc etc, for a ton of different types of operators and functions. This allows us to type e.g. expr3 := expr1.Add(expr2) which I think is nicer and less cluttered to write than expr3 := pkg.Add(expr1, expr2).

Now, the above is very simplified; we don't actually just do raw string interpolation, and this being an AST, we have many different node types which are all interrelated, meaning that (as I've only later come to realise) instantiating one node will also tend to instantiate a bunch of other related node types and their methods. We also use a lot of embedding in order to reuse functionality between node types (e.g. there are multiple different types of expressions), which essentially duplicates the method symbol names for each embedding type. We also define various interfaces to allow us to e.g. introspect expressions without having to know the Type parameter.

The Model type parameter above will be a Go representation of the model associated with the expression, which is a struct containing e.g. the various fields available for the model. If the struct is moderately sized (but still below the limit for hashing the symbol), this means that each method symbol name will have the entire struct definition copy pasted into the symbol name, quickly blowing up the size of the symbol strings.

Type switches of the form that you see in Func11 come into play in the code that parses the result of a query, in order to perform specialisation (more efficient parsers for certain types). The biggest such type switch is in fact already contained in a function rather than a method in our case, but as the function is called by code that is reachable by our AST nodes, it, along with the types used in the type switch, will generally be instantiated anyway.

The insidious part of this is that in practice, the issue really only starts to manifest at scale, when the generic types are already used pervasively throughout the codebase. This means that by the time you come to realise that there is a problem, it's very difficult and time consuming to refactor the code in order to soothe the compiler. I would also find it unfortunate if a pattern that I find very ergonomic to use while also adding a lot of type safety would be discouraged just because of a limitation of the compiler.

@timothy-king
Copy link
Contributor

@arvidfm Thank your for the additional context. I could see how one end up in this situation a bit better.

FWIW the point I was trying to make is that I think the more promising direction is to find a way to shift the asymptotic growth of the cache. String compression or sharing might shrink the object files (though possibly at the cost of readability and tool complexity), but it won't fundamentally change the growth rate for how many methods are generated. So I don't think this issue (long symbol names) is the most promising way of addressing your problem.

Addressing point 4 in #50438 (comment) would be a real shift in the asymptotics of the build cache in this example. So #56718 seems like the more relevant issue.

@arvidfm
Copy link

arvidfm commented Dec 10, 2024

@timothy-king Yes, I agree that #56718 is the more immediately pressing issue, and also what will result in the biggest short-term gains. I do think it's worth thinking about ways deal with this issue specifically as well though, since there are pathological cases where it would still crop up for individual compilation units even with the other issue fixed, e.g. if all your instantiations happen in the same package.

Probably the most promising avenues to explore in terms of this issue specifically would be finding ways to make the symbol names themselves shorter (lower threshold for hashing symbol names, creating a single hash for all type parameters instead of one per type parameter, abbreviating long package names, etc) as well as reducing the number of methods compiled to begin with (don't compile methods unless explicitly called or reachable from a type made into an interface value, avoid creating wrapper methods unless actually used, allow explicitly marking methods as not available to satisfy interfaces or to call via reflection), but I recognise neither is straightforward (especially the latter).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. generics Issue is related to generics NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
Development

No branches or pull requests