Repository navigation
cmd/compile: missed devirtualization of generic dictionary calls for closed/package-internal constraints #81377
Description
Go version
go version go1.26.7-X:nodwarf5 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='/usr/sbin/gccgo'
GO111MODULE='on'
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN='/home/ugorji/.local/bin'
GOCACHE='/home/ugorji/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/ugorji/.config/go/env'
GOEXE=''
GOEXPERIMENT='nodwarf5'
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2991509374=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/home/ugorji/repos/scratch/go.mod'
GOMODCACHE='/home/ugorji/.local/share/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/ugorji/.local/share/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/lib/golang'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/ugorji/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='local'
GOTOOLDIR='/usr/lib/golang/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.26.7-X:nodwarf5'
GOWORK='/home/ugorji/repos/go.work'
PKG_CONFIG='pkg-config'What did you do?
Compiled a package containing generic types parameterized over an unexported interface, instantiated solely with unexported concrete implementations where the generic method exceeds the inlining threshold.
Reproducer artifacts:
- repro.go: minimal reproducer
- repro.sh: reproducer script (run as ./repro.sh > repro.out)
- repro.out: output from running repro.sh
repro.sh:
go build -o repro .
go tool nm -size repro | grep "helperDecDriver.*decodeLarge"
go tool objdump -S -s "helperDecDriver.*decodeLarge" reprorepro.go:
package main
import "fmt"
type bytesDecReader struct{ data []byte }
func (r *bytesDecReader) readByte() byte {
if len(r.data) == 0 {
return 0
}
b := r.data[0]
r.data = r.data[1:]
return b
}
type ioDecReader struct{ data []byte }
func (r *ioDecReader) readByte() byte {
if len(r.data) == 0 {
return 0
}
b := r.data[0]
r.data = r.data[1:]
return b
}
type decReader interface {
*bytesDecReader | *ioDecReader
readByte() byte
}
type msgpackDecDriver[R decReader] struct{ r R }
func (d *msgpackDecDriver[R]) read() byte { return d.r.readByte() }
type jsonDecDriver[R decReader] struct{ r R }
func (d *jsonDecDriver[R]) read() byte { return d.r.readByte() }
type decDriver interface {
*msgpackDecDriver[*bytesDecReader] |
*msgpackDecDriver[*ioDecReader] |
*jsonDecDriver[*bytesDecReader] |
*jsonDecDriver[*ioDecReader]
read() byte
}
type helperDecDriver[D decDriver] struct{}
//go:noinline
func (h *helperDecDriver[D]) decodeLarge(driver D) byte {
// Represents large method (> 80 AST nodes, e.g. large type switch)
// var acc byte
// for i := 0; i < 100; i++ {
// acc += driver.read()
// }
// return acc
c := driver.read()
c++
return c
}
func main() {
h1 := &helperDecDriver[*msgpackDecDriver[*bytesDecReader]]{}
h2 := &helperDecDriver[*msgpackDecDriver[*ioDecReader]]{}
h3 := &helperDecDriver[*jsonDecDriver[*bytesDecReader]]{}
h4 := &helperDecDriver[*jsonDecDriver[*ioDecReader]]{}
fmt.Println(
h1.decodeLarge(&msgpackDecDriver[*bytesDecReader]{}),
h2.decodeLarge(&msgpackDecDriver[*ioDecReader]{}),
h3.decodeLarge(&jsonDecDriver[*bytesDecReader]{}),
h4.decodeLarge(&jsonDecDriver[*ioDecReader]{}),
)
}What did you see happen?
What did you see happen?
Full stencil duplication: The compiler emits distinct .text symbol instances for each shape permutation (e.g., main.(*helperDecDriver[go.shape.*main.msgpackDecDriver[*main.bytesDecReader]]).decodeLarge(SB), etc.).
Indirect dictionary dispatch remains: In the disassembled stencils, invocations on driver.read(...) are not direct static calls; they remain indirect calls loaded via the dictionary context register.
objdump confirms indirect call via dictionary context:
0x49e44e 488b33 MOVQ 0(BX), SI ; Load concrete method ptr from dictionary
0x49e451 4889c8 MOVQ CX, AX ; Pass receiver in AX (ABIInternal)
0x49e454 4889da MOVQ BX, DX ; Pass dictionary in context register DX
0x49e457 ffd6 CALL SI ; Indirect call (not static devirtualized)
In real-world libraries with large generic driver methods (e.g., a 25 KB decoding method), this duplicates hundreds of kilobytes of code across distinct driver/reader permutations without delivering the performance benefits of monomorphization. I am currently dealing with this in go-codec library, where the generic code binary is even bigger than the monomorphized/generated manually de-duplicated code.
Sample output below:
49e3e0 70 T main.(*helperDecDriver[go.shape.*main.jsonDecDriver[*main.bytesDecReader]]).decodeLarge
49e380 70 T main.(*helperDecDriver[go.shape.*main.jsonDecDriver[*main.ioDecReader]]).decodeLarge
49e4a0 70 T main.(*helperDecDriver[go.shape.*main.msgpackDecDriver[*main.bytesDecReader]]).decodeLarge
49e440 70 T main.(*helperDecDriver[go.shape.*main.msgpackDecDriver[*main.ioDecReader]]).decodeLarge
TEXT main.(*helperDecDriver[go.shape.*main.jsonDecDriver[*main.ioDecReader]]).decodeLarge(SB) /home/ugorji/repos/scratch/xx/repro.go
func (h *helperDecDriver[D]) decodeLarge(driver D) byte {
0x49e380 493b6610 CMPQ SP, 0x10(R14)
0x49e384 761b JBE 0x49e3a1
0x49e386 55 PUSHQ BP
0x49e387 4889e5 MOVQ SP, BP
0x49e38a 4883ec08 SUBQ $0x8, SP
c := driver.read()
0x49e38e 488b33 MOVQ 0(BX), SI
0x49e391 4889c8 MOVQ CX, AX
0x49e394 4889da MOVQ BX, DX
0x49e397 ffd6 CALL SI
c++
0x49e399 ffc0 INCL AX
return c
0x49e39b 4883c408 ADDQ $0x8, SP
0x49e39f 5d POPQ BP
0x49e3a0 c3 RET
func (h *helperDecDriver[D]) decodeLarge(driver D) byte {
0x49e3a1 4889442408 MOVQ AX, 0x8(SP)
0x49e3a6 48895c2410 MOVQ BX, 0x10(SP)
0x49e3ab 48894c2418 MOVQ CX, 0x18(SP)
0x49e3b0 e80bf5fdff CALL runtime.morestack_noctxt.abi0(SB)
0x49e3b5 488b442408 MOVQ 0x8(SP), AX
0x49e3ba 488b5c2410 MOVQ 0x10(SP), BX
0x49e3bf 488b4c2418 MOVQ 0x18(SP), CX
0x49e3c4 ebba JMP main.(*helperDecDriver[go.shape.*main.jsonDecDriver[*main.ioDecReader]]).decodeLarge(SB)
TEXT main.(*helperDecDriver[go.shape.*main.jsonDecDriver[*main.bytesDecReader]]).decodeLarge(SB) /home/ugorji/repos/scratch/xx/repro.go
func (h *helperDecDriver[D]) decodeLarge(driver D) byte {
0x49e3e0 493b6610 CMPQ SP, 0x10(R14)
0x49e3e4 761b JBE 0x49e401
0x49e3e6 55 PUSHQ BP
0x49e3e7 4889e5 MOVQ SP, BP
0x49e3ea 4883ec08 SUBQ $0x8, SP
c := driver.read()
0x49e3ee 488b33 MOVQ 0(BX), SI
0x49e3f1 4889c8 MOVQ CX, AX
0x49e3f4 4889da MOVQ BX, DX
0x49e3f7 ffd6 CALL SI
c++
0x49e3f9 ffc0 INCL AX
return c
0x49e3fb 4883c408 ADDQ $0x8, SP
0x49e3ff 5d POPQ BP
0x49e400 c3 RET
func (h *helperDecDriver[D]) decodeLarge(driver D) byte {
0x49e401 4889442408 MOVQ AX, 0x8(SP)
0x49e406 48895c2410 MOVQ BX, 0x10(SP)
0x49e40b 48894c2418 MOVQ CX, 0x18(SP)
0x49e410 e8abf4fdff CALL runtime.morestack_noctxt.abi0(SB)
0x49e415 488b442408 MOVQ 0x8(SP), AX
0x49e41a 488b5c2410 MOVQ 0x10(SP), BX
0x49e41f 488b4c2418 MOVQ 0x18(SP), CX
0x49e424 ebba JMP main.(*helperDecDriver[go.shape.*main.jsonDecDriver[*main.bytesDecReader]]).decodeLarge(SB)
TEXT main.(*helperDecDriver[go.shape.*main.msgpackDecDriver[*main.ioDecReader]]).decodeLarge(SB) /home/ugorji/repos/scratch/xx/repro.go
func (h *helperDecDriver[D]) decodeLarge(driver D) byte {
0x49e440 493b6610 CMPQ SP, 0x10(R14)
0x49e444 761b JBE 0x49e461
0x49e446 55 PUSHQ BP
0x49e447 4889e5 MOVQ SP, BP
0x49e44a 4883ec08 SUBQ $0x8, SP
c := driver.read()
0x49e44e 488b33 MOVQ 0(BX), SI
0x49e451 4889c8 MOVQ CX, AX
0x49e454 4889da MOVQ BX, DX
0x49e457 ffd6 CALL SI
c++
0x49e459 ffc0 INCL AX
return c
0x49e45b 4883c408 ADDQ $0x8, SP
0x49e45f 5d POPQ BP
0x49e460 c3 RET
func (h *helperDecDriver[D]) decodeLarge(driver D) byte {
0x49e461 4889442408 MOVQ AX, 0x8(SP)
0x49e466 48895c2410 MOVQ BX, 0x10(SP)
0x49e46b 48894c2418 MOVQ CX, 0x18(SP)
0x49e470 e84bf4fdff CALL runtime.morestack_noctxt.abi0(SB)
0x49e475 488b442408 MOVQ 0x8(SP), AX
0x49e47a 488b5c2410 MOVQ 0x10(SP), BX
0x49e47f 488b4c2418 MOVQ 0x18(SP), CX
0x49e484 ebba JMP main.(*helperDecDriver[go.shape.*main.msgpackDecDriver[*main.ioDecReader]]).decodeLarge(SB)
TEXT main.(*helperDecDriver[go.shape.*main.msgpackDecDriver[*main.bytesDecReader]]).decodeLarge(SB) /home/ugorji/repos/scratch/xx/repro.go
func (h *helperDecDriver[D]) decodeLarge(driver D) byte {
0x49e4a0 493b6610 CMPQ SP, 0x10(R14)
0x49e4a4 761b JBE 0x49e4c1
0x49e4a6 55 PUSHQ BP
0x49e4a7 4889e5 MOVQ SP, BP
0x49e4aa 4883ec08 SUBQ $0x8, SP
c := driver.read()
0x49e4ae 488b33 MOVQ 0(BX), SI
0x49e4b1 4889c8 MOVQ CX, AX
0x49e4b4 4889da MOVQ BX, DX
0x49e4b7 ffd6 CALL SI
c++
0x49e4b9 ffc0 INCL AX
return c
0x49e4bb 4883c408 ADDQ $0x8, SP
0x49e4bf 5d POPQ BP
0x49e4c0 c3 RET
func (h *helperDecDriver[D]) decodeLarge(driver D) byte {
0x49e4c1 4889442408 MOVQ AX, 0x8(SP)
0x49e4c6 48895c2410 MOVQ BX, 0x10(SP)
0x49e4cb 48894c2418 MOVQ CX, 0x18(SP)
0x49e4d0 e8ebf3fdff CALL runtime.morestack_noctxt.abi0(SB)
0x49e4d5 488b442408 MOVQ 0x8(SP), AX
0x49e4da 488b5c2410 MOVQ 0x10(SP), BX
0x49e4df 488b4c2418 MOVQ 0x18(SP), CX
0x49e4e4 ebba JMP main.(*helperDecDriver[go.shape.*main.msgpackDecDriver[*main.bytesDecReader]]).decodeLarge(SB)
What did you expect to see?
Because:
- The interface constraints (decReader, decDriver) are unexported and cannot be implemented outside this package.
- The implementing types (bytesDecReader, ioDecReader, msgpackDecDriver, jsonDecDriver) are unexported and cannot be instantiated by external packages.
- The method set is statically known and closed within the package, and explicitly defined in a type constraint
When cmd/compile/internal/noder/stencil.go instantiates these dedicated stencils, it has full compile-time knowledge of the target concrete method.
The compiler should devirtualize the dictionary lookup into a direct static CALL to the concrete method symbol (e.g., (*msgpackDecDriver[*bytesDecReader]).read). This would eliminate indirect call branch mispredictions and allow downstream SSA optimizations without violating shape-sharing invariants or package boundaries.
This is a very tight optimization boundary (completely within package boundary and explicitly limited via unexported constraints containing limited set of unexported generic types). Consequently, I hope it is allowed to go forward.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
gabyhelp commentedon Sep 7, 2026
Related Issues
Related Code Changes
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
JunyangShao commentedon Sep 11, 2026
@golang/compiler