Skip to content

runtime: move goroutine state into stack-rooted getg context#2074

Closed
cpunion wants to merge 10 commits into
xgo-dev:mainfrom
cpunion:codex/runtime-g-state
Closed

runtime: move goroutine state into stack-rooted getg context#2074
cpunion wants to merge 10 commits into
xgo-dev:mainfrom
cpunion:codex/runtime-g-state

Conversation

@cpunion

@cpunion cpunion commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Dependency

This PR is stacked on #2079 at 7f7ce3bd0. #2079 provides the current-context anchor and outer entry-context lifecycle. This PR only uses that facility to hold existing runtime goroutine state; it does not change locality directive semantics, storage planning, initialization, or lowering. The GitHub base remains main because the dependency branch is in a fork.

Problem

The pthread-hosted runtime currently spreads state owned by one goroutine across unrelated mechanisms:

  • the active defer chain uses a generic pthread-key handle;
  • the active panic uses a separate pthread key;
  • Goexit unwind uses another pthread key;
  • main-goroutine detection calls pthread_self/pthread_equal.

There is no common runtime g owner. Hot defer-chain reads take a dynamic pthread-key lookup, first use needs separate state setup, and each additional field needs another key or lifetime rule. #2079 alone does not migrate this existing runtime state, so these costs and ownership rules remain without this PR.

Design

Introduce one small runtime-owned g:

type g struct {
    defer_ *Defer
    panic_ unsafe.Pointer
    goexit bool
    isMain bool
}

On pthread-hosted LLGo targets, g is stored inline in the active outer entry context. getg loads the existing native TLS current-context anchor and returns the address of g at a fixed offset. The lookup has no pthread-key call, lock, allocation, hash, or dynamic search.

The runtime publishes LLGoNeedsLocalContext so generated entry wrappers install a context even when no source declaration otherwise requests one. This uses #2079's existing LOCAL_CONTEXT cache-fingerprint mode; it adds no compiler target-name check, exported compiler API, or build tag.

Bare-metal, host-side non-LLGo tests, and the current single-threaded nintendoswitch runtime use one process-global g. Nintendo Switch does not currently provide the pthread/native-TLS runtime required by the hosted representation. Its existing build tag selects the fallback entirely inside the runtime, preserving the empty-program target build without adding compiler-side target logic. A future task-context implementation can replace that fallback without changing getg callers.

Lifetime And Roots

main, LLGo-created goroutines, and exported Go entries install the outer context before runtime code can call getg. Exported entries are included because a C-created thread must acquire valid runtime state before entering Go. Nested entries reuse the active context.

Normal return restores the previous context. runtime.Goexit performs the same teardown before pthread_exit. The outer context lives in a thread stack scanned by BDWGC, so pointers held by g remain visible without RegisterLocalRoot, GC_add_roots, a pthread destructor, or a separately registered TLS range.

Cost

On a 64-bit pthread-hosted target:

Cost Amount
current-context anchor 8 native TLS bytes per thread
inline g 24 stack bytes per outer entry
total outer context after this PR 32 stack bytes
first g access allocation none
steady getg allocation none

The 24-byte g is the incremental context size added by this PR. A program that did not otherwise need an outer context pays the full 32-byte stack context and 8-byte thread anchor. The stack context is reserved once for each outer main/goroutine/exported entry, not for every Go function call.

On the final #2079 rebase, the retained GetThreadDefer benchmark measured the inline path at 1.265-1.286 ns/op (500ms x 5) versus the retained 2.321-2.337 ns/op pthread-key baseline, with 0 B/op and 0 allocs/op on both steady paths. An earlier separately allocated g prototype added roughly 64 B on first defer use; storing g inline removed that allocation.

Caller metadata, FIPS bypass depth, sync.Pool, and genuinely dynamic TLS handles are intentionally unchanged.

Validation

All local commands were serialized with GOMAXPROCS=2, GOMEMLIMIT=2GiB, GOFLAGS=-p=1, and a 15 GiB process-tree or container limit. No test was skipped or ignored.

  • macOS arm64, Go 1.24.11: full ssa, internal/build, and cl suites pass on the final compiler/runtime: add //llgo:tls and //llgo:gls package variables #2079 rebase; peak process-tree RSS was about 1.83 GiB.
  • macOS arm64, Go 1.26.5 driving LLGo: TestRuntimeGStateIsolation and full test/llgoext pass; peak process-tree RSS was about 1.51 GiB and 1.37 GiB respectively.
  • Ubuntu 24.04 arm64 container, Go 1.26.5 and LLVM 19, 2 CPUs/15 GiB: locality and runtime-context tests, TestRuntimeGStateIsolation, and full test/llgoext pass.
  • The same constrained Ubuntu/LLVM 19 container reproduces the pre-fix nintendoswitch empty-program dlfcn.h cross-compile failure. The final rebased branch builds that target successfully; no target was added to an ignore list.
  • The Ubuntu image's unrestricted host golden suite retains pre-existing arm64 alignment expectations (align 1 versus emitted align 4) and root-permission assumptions; these reproduce outside this PR and were not hidden by changing or skipping tests.
  • Changed ssa production lines are covered 13/13 (100%, no partial branches). The changed cl files are codegen goldens rather than production code.
  • Codecov patch coverage is 100.0% (919 hits, 0 misses, 0 partials). A local profile also executes both regions of cl/compile.go:1713, which was the old stacked head's only partial line.
  • Runtime behavior covers overlapping goroutine panic/defer chains, recovery isolation, child Goexit, defer order, and main-state isolation.
  • Codegen checks assert the 32-byte context and paired Enter/Leave calls for goroutine and C-export entry paths.

Commits

  1. runtime: consolidate goroutine state behind getg
  2. test: cover runtime g state isolation

@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cpunion
cpunion force-pushed the codex/runtime-g-state branch from 43d891a to acdfbaf Compare July 13, 2026 10:28
@cpunion cpunion changed the title runtime: consolidate per-goroutine state behind getg runtime: consolidate per-goroutine state behind TLS getg Jul 13, 2026
@cpunion
cpunion force-pushed the codex/runtime-g-state branch 6 times, most recently from 763f657 to 3c2395b Compare July 14, 2026 02:07
@cpunion cpunion changed the title runtime: consolidate per-goroutine state behind TLS getg runtime: move goroutine state into stack-rooted getg context Jul 14, 2026
@cpunion
cpunion marked this pull request as draft July 14, 2026 04:39
@cpunion
cpunion force-pushed the codex/runtime-g-state branch 2 times, most recently from b6260f1 to 397bb99 Compare July 14, 2026 14:31
@cpunion
cpunion force-pushed the codex/runtime-g-state branch from 397bb99 to 89701f5 Compare July 15, 2026 00:36
@cpunion

cpunion commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #2090, which implements the runtime g state directly with the //llgo:gls facility from #2079.

@cpunion cpunion closed this Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant