Skip to content

[Based on #2079] runtime: store goroutine state in //llgo:gls#2090

Open
cpunion wants to merge 15 commits into
xgo-dev:mainfrom
cpunion:codex/runtime-g-state-gls
Open

[Based on #2079] runtime: store goroutine state in //llgo:gls#2090
cpunion wants to merge 15 commits into
xgo-dev:mainfrom
cpunion:codex/runtime-g-state-gls

Conversation

@cpunion

@cpunion cpunion commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Depends on #2079

Dependency

This PR is stacked on #2079 at bf50d5347. #2079 provides //llgo:gls and the local-context lifecycle used here. The GitHub base remains main because the dependency branch is in a fork. The runtime-state implementation is 37e6cd4a3; the follow-up pure-C linkage regression fix is d5bf6b8de.

Problem

The hosted runtime currently stores state owned by one goroutine in several pthread-backed locations:

  • the active defer chain uses one pthread-key-backed handle;
  • panic and Goexit state use separate pthread keys;
  • main-goroutine detection calls pthread_self and pthread_equal.

This gives related state separate ownership and lifetime rules, adds pthread-key lookups to the defer path, and makes every new per-goroutine field require more runtime machinery.

Change

Store the state in one runtime-owned g:

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

//llgo:gls
var currentG g

getg returns &currentG; defer, panic/recover, Goexit, and main detection access fields of that object. This removes the three pthread keys, the pthread-backed defer handle, and mainThread.

On hosted LLGo targets, currentG is goroutine-local through #2079. Host builds and the currently single-context bare-metal and Nintendo Switch targets use the same g representation backed by one ordinary global variable. No target logic is added to the compiler.

The variable is zero-initialized. Normal package initialization marks only the initial context as main. A new goroutine gets zeroed runtime-package storage when it first reaches getg; normal entry/exit and the non-returning Goexit path use #2079's existing context lifecycle.

C library startup

With currentG in GLS, runtime.init became the first runtime path that requires a local context. C library constructors previously called it before C entered an exported Go function; on Linux c-archive, that crashed before main. Commit 34387d149 makes c-shared and c-archive constructors install a process-lifetime context before runtime.init. Exported calls on the constructor thread inherit it, while calls from other C threads keep their entry-scoped contexts.

Runtime cost

Apple M4 Max, macOS arm64, Go 1.26.5, LLVM 19.1.7, GOMAXPROCS=2. Results are medians of 8 adjacent 750 ms samples. The hot accessor reports 0 B/op and 0 allocs/op; the goroutine rows report the B/op values below and 0 allocs/op.

Measurement #2079 pthread-key This PR
hot GetThreadDefer 2.315 ns/op (2.305-2.327) 1.485 ns/op (1.424-1.505)
goroutine without defer 1603 B/op (1564-1606) 1597 B/op (1566-1604)
goroutine with first defer 1674 B/op (1654-1705) 1739 B/op (1710-1756)

The hot accessor is about 35.9% faster than the pthread-key baseline. Goroutines that do not touch the runtime g show no space regression. First defer use is about 65 B/goroutine higher because this PR intentionally leaves cold package-block allocation unoptimized: the 24-byte g payload also needs the shared block header, alignment, and allocator rounding.

Coverage and validation

The root Codecov job does not instrument production files in the nested runtime module, so a passing patch percentage alone does not demonstrate these runtime paths. The previous 84.17722% Codecov report was generated after CI was cancelled: the macOS root coverage test was interrupted, and Ubuntu completed the test but was cancelled before upload, so Codecov only had the Dev LTO subset. Local atomic coverage on the current stacked head reports 88.2% for ssa; every function previously listed as missing in ssa/locality.go, ssa/decl.go, and ssa/package.go is 100% covered. Head 37e6cd4a3 completed both root coverage jobs, and Codecov reported that all modified and coverable lines were covered. Head d5bf6b8de is rerunning CI; locally, the affected entry-module paths reach 85.3% function coverage and the original Linux/amd64 pure-C PCLN integration failure passes. One macOS primary shard first hit the 30-minute job limit without a test error, then passed on the targeted rerun in 18m49s. The nested-runtime implementation is instead exercised by real LLGo integration tests:

  • TestRuntimeGStateIsolation exercises concurrent defer ordering, panic/recover isolation, Goexit, and main-goroutine state.
  • TestRuntimeDeferHeadIsolation observes real active defer frames in two goroutines, verifies distinct heads, and verifies restoration after return.
  • TestRuntimeDeferAccessors exercises the Set/Get/Clear state transitions independently in two goroutines.
  • LLVM FileCheck fixtures verify context entry/exit code generation for Go and C entry points.

Local validation was serialized with GOMAXPROCS=2 and GOFLAGS=-p=1; no existing test was skipped or ignored.

  • macOS arm64, Go 1.26.5: current-source install, affected SSA/codegen fixtures, full test/go, and full test/llgoext pass; the new defer tests also pass 100 repetitions.
  • Ubuntu 24.04 arm64, Go 1.26.5, LLVM 19.1.7: affected compiler/SSA tests, actual selects execution, full test/go, and full test/llgoext pass in a container limited to 2 CPU and 512 PID, with a requested 15 GiB cap and an effective 11.66 GiB Docker VM cap.
  • ESP32-C3 builds a defer-using program; Nintendo Switch builds the global fallback.

Commits

  1. runtime: store goroutine state in GLS (37e6cd4a3)
  2. build: avoid runtime linkage for pure C programs (d5bf6b8de)
  3. runtime: initialize GLS context for C libraries (34387d149)

Implements the runtime-state consumer described in #2077 on top of #2079.

@codecov

codecov Bot commented Jul 14, 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-gls branch 2 times, most recently from b543675 to 57ae899 Compare July 15, 2026 00:48
@cpunion
cpunion force-pushed the codex/runtime-g-state-gls branch from 57ae899 to b651488 Compare July 15, 2026 08:54
@cpunion
cpunion marked this pull request as ready for review July 15, 2026 10:22
@cpunion
cpunion force-pushed the codex/runtime-g-state-gls branch 2 times, most recently from 63fe488 to 56564e7 Compare July 15, 2026 20:52
@cpunion cpunion changed the title runtime: store goroutine state in //llgo:gls [Based on #2079] runtime: store goroutine state in //llgo:gls Jul 16, 2026
@cpunion
cpunion force-pushed the codex/runtime-g-state-gls branch 3 times, most recently from 353c562 to e363bbe Compare July 18, 2026 23:20
@cpunion
cpunion force-pushed the codex/runtime-g-state-gls branch from b373c9d to a309b55 Compare July 20, 2026 03:40
cpunion added 5 commits July 20, 2026 12:09
Give each package-backed locality block an owner-local TLS cache so hot accesses are O(1) regardless of the number of touched packages. Keep the block list only for GC reachability and teardown.

Require locality variables to be unexported and reject go:linkname aliases. Add codegen, lifecycle, and 2/4/8-package performance coverage.
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