Skip to content

cmd/compile: unexpected heap allocation for large structs in Go 1.24 compared to Go 1.23 #73536

@ritwikranjan

Description

@ritwikranjan

Environment:

➜  test git:(master) ✗ go env                                                                                                                                                                                                                                                                                                                            git:(master|✚4…3 
GO111MODULE=''
GOARCH='amd64'
GOBIN=''
GOCACHE='/home/ritwikranjan/.cache/go-build'
GOENV='/home/ritwikranjan/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/home/ritwikranjan/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/ritwikranjan/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/ritwikranjan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.8.linux-amd64'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/ritwikranjan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.8.linux-amd64/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.23.8'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/ritwikranjan/.config/go/telemetry'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='1'
GOMOD='/home/ritwikranjan/workplace/ethtool/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build1704673884=/tmp/go-build -gno-record-gcc-switches'

Problem Description:

When compiling the same code containing large struct variables within a function, Go 1.23 allocates these structs on the stack, while Go 1.24 allocates them on the heap (they escape).

Code Example:

package main

// Large struct (~1MB)
type ethtoolGStrings struct {
	cmd        uint32
	string_set uint32
	len        uint32
	data       [32768 * 32]byte // Approx 1MB
}

// Large struct (~256KB)
type ethtoolStats struct {
	cmd     uint32
	n_stats uint32
	data    [32768]uint64 // Approx 256KB
}

func function(x int) int {
	// Variables declared locally
	a := ethtoolGStrings{}
	b := ethtoolStats{}

	// Trivial use to prevent optimization removing them entirely
	if a.cmd == 0 {
		return 0
	}
	if b.cmd == 0 {
		return 0
	}

	return x
}

func main() {
	function(1)
}

Steps to Reproduce:

  1. Save the code above as main.go.

  2. Compile with Go 1.23 using escape analysis enabled:

    go build -gcflags="-m" main.go

    Output

    ➜  test git:(master) ✗ go build -gcflags="-m"                                                                                                                                                                                                                                                                                                            git:(master|✚4…2 
    # github.com/safchain/ethtool/test
    ./main.go:16:6: can inline function
    ./main.go:30:6: can inline main
    ./main.go:31:10: inlining call to function
    
  3. Compile with Go 1.24 using escape analysis enabled:

    go build -gcflags="-m" main.go

    Output

    ➜  test git:(master) ✗ go build -gcflags="-m"                                                                                                                                                                                                                                                                                                            git:(master|✚4…2 
    # github.com/safchain/ethtool/test
    ./main.go:16:6: can inline function
    ./main.go:30:6: can inline main
    ./main.go:31:10: inlining call to function
    ./main.go:17:2: moved to heap: a
    ./main.go:18:2: moved to heap: b
    ./main.go:31:10: moved to heap: a
    ./main.go:31:10: moved to heap: b
    

Observed Behavior:

  • Go 1.23: The output from -gcflags="-m" does not indicate that variables a and b escape to the heap. They appear to be stack-allocated as expected for local variables whose addresses don't escape.
  • Go 1.24: The output from -gcflags="-m" does indicate that variables a and b escape or are moved to the heap (e.g., showing lines like ./main.go:17:2: moved to heap: a and ./main.go:18:2: moved to heap: b).
  • Analysis of the assembler output (go build -gcflags="-S") for both versions confirms the difference in allocation strategy.
go1.24 go1.23

    main.function STEXT size=108 args=0x8 locals=0x20 funcid=0x0 align=0x0
        0x0000 00000 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     TEXT    main.function(SB), ABIInternal, $32-8
        0x0000 00000 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     CMPQ    SP, 16(R14)
        0x0004 00004 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     PCDATA  $0, $-2
        0x0004 00004 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     JLS     90
        0x0006 00006 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     PCDATA  $0, $-1
        0x0006 00006 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     PUSHQ   BP
        0x0007 00007 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     MOVQ    SP, BP
        0x000a 00010 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     SUBQ    $24, SP
        0x000e 00014 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     FUNCDATA        $0, gclocals·ISb46fRPFoZ9pIfykFK/kQ==(SB)
        0x000e 00014 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     FUNCDATA        $1, gclocals·jCgrU8XAg0ifiSJZPFgpKw==(SB)
        0x000e 00014 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     FUNCDATA        $5, main.function.arginfo1(SB)
        0x000e 00014 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     FUNCDATA        $6, main.function.argliveinfo(SB)
        0x000e 00014 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     PCDATA  $3, $1
        0x000e 00014 (/home/ritwikranjan/workplace/ethtool/test/main.go:20)     MOVQ    AX, main.x+40(SP)
        0x0013 00019 (/home/ritwikranjan/workplace/ethtool/test/main.go:20)     PCDATA  $3, $-1
        0x0013 00019 (/home/ritwikranjan/workplace/ethtool/test/main.go:17)     LEAQ    type:main.ethtoolGStrings(SB), AX
        0x001a 00026 (/home/ritwikranjan/workplace/ethtool/test/main.go:17)     PCDATA  $1, $0
        0x001a 00026 (/home/ritwikranjan/workplace/ethtool/test/main.go:17)     CALL    runtime.newobject(SB)
        0x001f 00031 (/home/ritwikranjan/workplace/ethtool/test/main.go:17)     MOVQ    AX, main.&a+16(SP)
        0x0024 00036 (/home/ritwikranjan/workplace/ethtool/test/main.go:18)     LEAQ    type:main.ethtoolStats(SB), AX
        0x002b 00043 (/home/ritwikranjan/workplace/ethtool/test/main.go:18)     PCDATA  $1, $1
        0x002b 00043 (/home/ritwikranjan/workplace/ethtool/test/main.go:18)     CALL    runtime.newobject(SB)
        0x0030 00048 (/home/ritwikranjan/workplace/ethtool/test/main.go:20)     MOVQ    main.&a+16(SP), CX
        0x0035 00053 (/home/ritwikranjan/workplace/ethtool/test/main.go:20)     CMPL    (CX), $0
        0x0038 00056 (/home/ritwikranjan/workplace/ethtool/test/main.go:20)     JEQ     82
        0x003a 00058 (/home/ritwikranjan/workplace/ethtool/test/main.go:23)     CMPL    (AX), $0
        0x003d 00061 (/home/ritwikranjan/workplace/ethtool/test/main.go:23)     JNE     71
        0x003f 00063 (/home/ritwikranjan/workplace/ethtool/test/main.go:24)     XORL    AX, AX
        0x0041 00065 (/home/ritwikranjan/workplace/ethtool/test/main.go:24)     ADDQ    $24, SP
        0x0045 00069 (/home/ritwikranjan/workplace/ethtool/test/main.go:24)     POPQ    BP
        0x0046 00070 (/home/ritwikranjan/workplace/ethtool/test/main.go:24)     RET
        0x0047 00071 (/home/ritwikranjan/workplace/ethtool/test/main.go:27)     MOVQ    main.x+40(SP), AX
        0x004c 00076 (/home/ritwikranjan/workplace/ethtool/test/main.go:27)     ADDQ    $24, SP
        0x0050 00080 (/home/ritwikranjan/workplace/ethtool/test/main.go:27)     POPQ    BP
        0x0051 00081 (/home/ritwikranjan/workplace/ethtool/test/main.go:27)     RET
        0x0052 00082 (/home/ritwikranjan/workplace/ethtool/test/main.go:21)     XORL    AX, AX
        0x0054 00084 (/home/ritwikranjan/workplace/ethtool/test/main.go:21)     ADDQ    $24, SP
        0x0058 00088 (/home/ritwikranjan/workplace/ethtool/test/main.go:21)     POPQ    BP
        0x0059 00089 (/home/ritwikranjan/workplace/ethtool/test/main.go:21)     RET
        0x005a 00090 (/home/ritwikranjan/workplace/ethtool/test/main.go:21)     NOP
        0x005a 00090 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     PCDATA  $1, $-1
        0x005a 00090 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     PCDATA  $0, $-2
        0x005a 00090 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     MOVQ    AX, 8(SP)
        0x005f 00095 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     NOP
        0x0060 00096 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     CALL    runtime.morestack_noctxt(SB)
        0x0065 00101 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     PCDATA  $0, $-1
        0x0065 00101 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     MOVQ    8(SP), AX
        0x006a 00106 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     JMP     0
    
    main.function STEXT size=86 args=0x8 locals=0x100018 funcid=0x0 align=0x0
        0x0000 00000 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     TEXT    main.function(SB), ABIInternal, $1048600-8
        0x0000 00000 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     MOVQ    SP, R12
        0x0003 00003 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     PCDATA  $0, $-2
        0x0003 00003 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     SUBQ    $1048472, R12
        0x000a 00010 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     JCS     79
        0x000c 00012 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     CMPQ    R12, 16(R14)
        0x0010 00016 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     JLS     79
        0x0012 00018 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     PCDATA  $0, $-1
        0x0012 00018 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     PUSHQ   BP
        0x0013 00019 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     MOVQ    SP, BP
        0x0016 00022 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     SUBQ    $1048592, SP
        0x001d 00029 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     FUNCDATA        $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
        0x001d 00029 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     FUNCDATA        $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
        0x001d 00029 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     FUNCDATA        $5, main.function.arginfo1(SB)
        0x001d 00029 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     FUNCDATA        $6, main.function.argliveinfo(SB)
        0x001d 00029 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     PCDATA  $3, $1
        0x001d 00029 (/home/ritwikranjan/workplace/ethtool/test/main.go:17)     MOVUPS  X15, main.a+4(SP)
        0x0023 00035 (/home/ritwikranjan/workplace/ethtool/test/main.go:17)     LEAQ    main.a+16(SP), DI
        0x0028 00040 (/home/ritwikranjan/workplace/ethtool/test/main.go:17)     MOVL    $131072, CX
        0x002d 00045 (/home/ritwikranjan/workplace/ethtool/test/main.go:17)     XORL    AX, AX
        0x002f 00047 (/home/ritwikranjan/workplace/ethtool/test/main.go:17)     REP
        0x0030 00048 (/home/ritwikranjan/workplace/ethtool/test/main.go:17)     STOSQ
        0x0032 00050 (/home/ritwikranjan/workplace/ethtool/test/main.go:20)     CMPL    main.a+4(SP), $0
        0x0037 00055 (/home/ritwikranjan/workplace/ethtool/test/main.go:20)     JNE     68
        0x0039 00057 (/home/ritwikranjan/workplace/ethtool/test/main.go:21)     XORL    AX, AX
        0x003b 00059 (/home/ritwikranjan/workplace/ethtool/test/main.go:21)     ADDQ    $1048592, SP
        0x0042 00066 (/home/ritwikranjan/workplace/ethtool/test/main.go:21)     POPQ    BP
        0x0043 00067 (/home/ritwikranjan/workplace/ethtool/test/main.go:21)     RET
        0x0044 00068 (/home/ritwikranjan/workplace/ethtool/test/main.go:24)     XORL    AX, AX
        0x0046 00070 (/home/ritwikranjan/workplace/ethtool/test/main.go:24)     ADDQ    $1048592, SP
        0x004d 00077 (/home/ritwikranjan/workplace/ethtool/test/main.go:24)     POPQ    BP
        0x004e 00078 (/home/ritwikranjan/workplace/ethtool/test/main.go:24)     RET
        0x004f 00079 (/home/ritwikranjan/workplace/ethtool/test/main.go:24)     NOP
        0x004f 00079 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     PCDATA  $1, $-1
        0x004f 00079 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     PCDATA  $0, $-2
        0x004f 00079 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     CALL    runtime.morestack_noctxt(SB)
        0x0054 00084 (/home/ritwikranjan/workplace/ethtool/test/main.go:16)     PCDATA  $0, $-1
        

Expected Behavior:

It was expected that the allocation behavior for these local structs would remain consistent between Go versions, primarily remaining on the stack, unless there's a documented change in escape analysis or stack size limits causing this. If this is an intentional change in Go 1.24 (perhaps related to stack size management or PGO), it would be helpful to have it documented.

Additional Context:

  • I could not find any specific mention of this change in the Go 1.24 release notes or other documentation.
  • The structs ethtoolGStrings and ethtoolStats are relatively large (approx 1MB and 256KB, respectively). Is there a new size threshold or heuristic in Go 1.24 causing large local variables to be heap-allocated by default?
  • Is there a compiler flag or other mechanism in Go 1.24 to influence this behavior or revert to the previous stack allocation strategy for such cases, assuming the stack size limit isn't exceeded?

Thank you for looking into this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.FrozenDueToAgecompiler/runtimeIssues related to the Go compiler and/or runtime.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions