Skip to content

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

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

Closed
ritwikranjan opened this issue Apr 29, 2025 · 3 comments
Labels
BugReport Issues describing a possible bug in the Go implementation. compiler/runtime Issues related to the Go compiler and/or runtime.

Comments

@ritwikranjan
Copy link

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/[email protected]'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/ritwikranjan/go/pkg/mod/golang.org/[email protected]/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.

@seankhliao seankhliao changed the title Unexpected heap allocation for large structs in Go 1.24 compared to Go 1.23 cmd/compile: unexpected heap allocation for large structs in Go 1.24 compared to Go 1.23 Apr 29, 2025
@gopherbot gopherbot added the compiler/runtime Issues related to the Go compiler and/or runtime. label Apr 29, 2025
@gabyhelp gabyhelp added the BugReport Issues describing a possible bug in the Go implementation. label Apr 29, 2025
@Jorropo
Copy link
Member

Jorropo commented Apr 29, 2025

This is due to 5a0f2a7

git bisect start
# status: waiting for both good and bad commits
# good: [6885bad7dd86880be6929c02085e5c7a67ff2887] [release-branch.go1.23] go1.23.0
git bisect good 6885bad7dd86880be6929c02085e5c7a67ff2887
# status: waiting for bad commit, 1 good commit known
# bad: [6885bad7dd86880be6929c02085e5c7a67ff2887] [release-branch.go1.23] go1.23.0

# nice -n19 git bisect run bash -c "cd src && ./make.bash && go build -gcflags=-m /tmp/a.go 2>&1 | grep -F 'moved to heap' > /dev/null && exit 1 || exit 0"

git bisect bad 6885bad7dd86880be6929c02085e5c7a67ff2887
# bad: [1e756dc5f73dc19eb1cbf038807d18ef1cc54ebc] cmd/compile: relax tighten register-pressure heuristic slightly
git bisect bad 1e756dc5f73dc19eb1cbf038807d18ef1cc54ebc
# bad: [9776d028f4b99b9a935dae9f63f32871b77c49af] crypto/ecdsa: implement deterministic and hedged signatures
git bisect bad 9776d028f4b99b9a935dae9f63f32871b77c49af
# good: [80143607f06fd6410700e9764cfea9aaac9c311c] internal/syscall/unix: allow calling getrandom(..., 0, ...)
git bisect good 80143607f06fd6410700e9764cfea9aaac9c311c
# good: [2bb820fd5be238c73e260011dbe4bd76f5c9313b] cmd/go: if GOPATH and GOROOT are the same, refer to wiki page
git bisect good 2bb820fd5be238c73e260011dbe4bd76f5c9313b
# good: [18c2461af38e93ed385e953f3336fcaaca2da727] runtime: allow futex OSes to use sema-based mutex
git bisect good 18c2461af38e93ed385e953f3336fcaaca2da727
# bad: [7d7618971eeb244ca062f848941d9d890d21f9f9] crypto/aes,crypto/cipher: test all available implementations
git bisect bad 7d7618971eeb244ca062f848941d9d890d21f9f9
# good: [d4b0bd28eef0a212930fb196230171a9f11e5ec4] internal/runtime/maps: don't copy indirect key/elem when growing maps
git bisect good d4b0bd28eef0a212930fb196230171a9f11e5ec4
# bad: [e5c4c79cc446976e61f0b08577dc6f994dec023f] internal/sync: add Clear to HashTrieMap
git bisect bad e5c4c79cc446976e61f0b08577dc6f994dec023f
# bad: [e51a33a0efa5883a9be5c46e95554a52070cb696] internal/sync: factor out lookup for CompareAndDelete in HashTrieMap
git bisect bad e51a33a0efa5883a9be5c46e95554a52070cb696
# bad: [bb2a5f0556fd6bb4dbbce5eef2d6317d20796ade] cmd: change from sort functions to slices functions
git bisect bad bb2a5f0556fd6bb4dbbce5eef2d6317d20796ade
# bad: [53b2b64b649b26c7bb3397bec5d86d3b203eb015] sync: add explicit noCopy fields to Map, Mutex, and Once
git bisect bad 53b2b64b649b26c7bb3397bec5d86d3b203eb015
# bad: [5a0f2a7a7c5658f4f3065c265cee61ec1bde9691] cmd/compile: remove gc programs from stack frame objects
git bisect bad 5a0f2a7a7c5658f4f3065c265cee61ec1bde9691
# first bad commit: [5a0f2a7a7c5658f4f3065c265cee61ec1bde9691] cmd/compile: remove gc programs from stack frame objects

@randall77
Copy link
Contributor

The maximum size of a stack-allocated object was lowered to 128KB (from 10MB).
This was due to the simplifications it provided in implementing large stack-allocated types.

We do not make any particular promises about the size at which stack or heap allocations happen. The size threshold (or perhaps other deciding factors other than size) may change from release to release.

There is no flag to alter this behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BugReport Issues describing a possible bug in the Go implementation. compiler/runtime Issues related to the Go compiler and/or runtime.
Projects
None yet
Development

No branches or pull requests

5 participants