compiler/runtime: add //llgo:tls and //llgo:gls package variables#2079
Open
cpunion wants to merge 12 commits into
Open
compiler/runtime: add //llgo:tls and //llgo:gls package variables#2079cpunion wants to merge 12 commits into
cpunion wants to merge 12 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
cpunion
force-pushed
the
codex/threadlocal-getg
branch
from
July 13, 2026 09:54
4d83bb5 to
319d298
Compare
cpunion
force-pushed
the
codex/threadlocal-getg
branch
2 times, most recently
from
July 13, 2026 10:26
a811aba to
f2d92a0
Compare
cpunion
force-pushed
the
codex/threadlocal-getg
branch
3 times, most recently
from
July 13, 2026 11:44
91078ba to
ec4ea02
Compare
cpunion
marked this pull request as ready for review
July 13, 2026 11:48
cpunion
force-pushed
the
codex/threadlocal-getg
branch
from
July 13, 2026 13:07
ec4ea02 to
ec34002
Compare
cpunion
force-pushed
the
codex/threadlocal-getg
branch
from
July 13, 2026 17:52
ec34002 to
973b556
Compare
cpunion
force-pushed
the
codex/threadlocal-getg
branch
from
July 13, 2026 23:00
973b556 to
cdde104
Compare
cpunion
marked this pull request as draft
July 14, 2026 11:10
cpunion
force-pushed
the
codex/threadlocal-getg
branch
from
July 14, 2026 11:13
7e1681b to
d626fca
Compare
cpunion
marked this pull request as ready for review
July 14, 2026 11:57
cpunion
force-pushed
the
codex/threadlocal-getg
branch
from
July 14, 2026 11:59
d626fca to
c83695a
Compare
cpunion
force-pushed
the
codex/threadlocal-getg
branch
4 times, most recently
from
July 15, 2026 13:23
61a919f to
1cdbd2b
Compare
12 tasks
This was referenced Jul 16, 2026
cpunion
force-pushed
the
codex/threadlocal-getg
branch
2 times, most recently
from
July 18, 2026 23:17
8c79ad7 to
5463a62
Compare
cpunion
force-pushed
the
codex/threadlocal-getg
branch
from
July 20, 2026 03:33
5463a62 to
5d83743
Compare
Store each immutable Func as one atomic pointer and validate the queried PC from the Func itself. This prevents concurrent cache replacement from combining a PC and function from different writers. Apply the same publication rule to the prebuilt function cache and stress multiple PCs during concurrent first use.
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.
cpunion
force-pushed
the
codex/threadlocal-getg
branch
from
July 20, 2026 04:09
5d83743 to
bf50d53
Compare
This was referenced Jul 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
LLGo runtime state is often owned by one pthread or goroutine, but package code
currently has to use
pthread.Keyaccessors or process globals. That adds keymanagement and lookup overhead, prevents ordinary Go variable access, and makes
GC ownership difficult for pointer-bearing state.
Without this PR, LLGo cannot declare addressable thread-local or goroutine-local
package variables with normal Go syntax and a GC-rooted lifetime model.
This PR implements #2077:
TLS and GLS remain separate source and initializer kinds. They share the current
physical backend because LLGo maps one goroutine to one pthread; GLS can move to
a future goroutine context without changing declarations.
Locality variables are package-level and unexported. They cannot use
go:linkname,go:embed, or_. Cross-package use goes through ordinaryexported functions compiled in the package that owns the variable.
Implementation
Storage and hot access
thread_localstorage.payload.
uintptrcache and onealways-inline accessor:
The hit path is O(1): load the package TLS cache, branch on zero, and return the
payload. It does not inspect another package, traverse a list, acquire a lock,
or call
pthread_getspecific.The runtime keeps a linked list only for GC reachability and owner teardown. It
is allocation order, not an LRU or lookup structure. On first touch,
runtime.LocalPackageallocates and roots the package block, then fills the TLScache. On exit, the runtime walks the root list once, clears each recorded cache
slot, and severs links. Escaped variable addresses remain valid without
retaining unrelated blocks.
The TLS cache is a non-root
uintptr;LocalContext.blocksis the stack GCroot. Each 64-bit block header remains 16 bytes.
Final macOS arm64 machine code confirms
AlwaysInlineis effective:__llgo_local_blockis eliminated, the cache check appears in the caller, andonly the miss branch calls
runtime.LocalPackage.Compilation and entry flow
internal/directiveparses directives generically.internal/localityowns declaration validation and initializer preparation.internal/locality/layoutis a pure type/layout planner.ssaowns program-wide metadata and local-context/accessor code generation.cl/locality_lower.gointegrates the plan into normal variable lowering.internal/buildpreloads source metadata before cached package compilationand fingerprints local-context entry mode.
Each package defines its own cache/accessor, TLS globals, initializer guards,
dispatcher, and ensure functions before body lowering. Package bases and
successful initializer checks are reused along SSA dominator paths.
The generated main entry installs a
LocalContextbefore package initialization.Each pthread wrapper generated for
go f(...)installs a fresh context, andC-exported Go entries use the same candidate enter/leave sequence. Nested entries
reuse an active context.
flowchart LR A[Source prepass] --> B[Initializer and layout plan] B --> C[Compile or load package modules] C --> D[Package TLS, cache, accessor, guards] B --> E[Instrument main, goroutine, exported entries] D --> F[Link runtime, packages, main] E --> FThe compiler does not infer support from target names or introduce a new target
tag. It emits LLVM TLS when selected; runtime source constraints continue to
choose hosted versus bare-metal runtime implementations.
Initialization
Explicit local variable initializers execute normally for the initial main
owner. A new owner replays them lazily on first access to that package/locality
kind:
There is one dispatcher and one guard per package/locality kind, not per
variable. Dispatchers preserve Go initialization order. Recursive access sees
partial initialization. A failed initializer stays failed and re-panics with
the saved value, including
panic(nil); failure payload storage is allocatedonly on panic. Go
func init()is never replayed.Cost model
anyblockPrograms containing only zero-initialized pointer-free native TLS locals need no
LocalContext. Cold first-touch allocation is deliberately left out of thisoptimization.
Measurements
Apple M4 Max, macOS arm64, Go 1.26.5, LLVM 19.1.7, real LLGo,
GOMAXPROCS=2,-count=8 -benchtime=750ms -benchmemunless noted.Comparable imported
//go:noinline func() uintptrreaders:All report
0 B/op, 0 allocs/op. Direct GLS is about 0.118 ns, or 9.1%,over native TLS in this run.
The retained pre-migration runtime benchmark records the old pthread-key cost:
GetThreadDefer#2079 intentionally leaves
GetThreadDeferon its existingpthread_getspecificimplementation, so this is a reproducible old-runtimebaseline for the follow-up migration. It is not the same function as the
generic reader table above; a same-function comparison belongs in the
runtime-state follow-up.
The retained alternating benchmark performs two serialized GLS reads per op.
One of its packages also has explicit GLS initializers, so that read checks the
shared package/kind initializer guard before reading its payload:
This is an apples-to-apples backend regression benchmark: direct caches reduce
the same guarded workload by 52.1%. It is not a pure two-read latency estimate.
The zero-initializer independent-package benchmark below is the scaling check.
Each independent working-set op is one batch containing N independent
Readcalls, so batchns/opis expected to grow with N:The per-read throughput does not increase as the working set grows from 2 to 8
packages, confirming that hot access no longer depends on touched-package
count. The slight decrease comes from independent calls overlapping; this
table measures throughput scaling, not single-read latency below the 1.418
ns/op result.
Fixed-count goroutine measurements also show no space regression: entry is
1552-1560 B/op versus the previous 1570-1572 B/op, and first package touch is
1583-1591 B/op versus 1592-1606 B/op. These counters are allocator/run-sensitive;
the relevant conclusion is that the direct cache did not add measured per-entry
allocation.
Final macOS arm64 disassembly contains no
__llgo_local_blocksymbol afteroptimization. The hit sequence is inlined into
ReadGLSPackage; only the zerocache branch calls
runtime.LocalPackage.Validation
All heavy local work used low parallelism; Ubuntu containers were capped at 2
CPU and 15 GiB.
ssapasses;cl/_testgo/localitycodegenpass;test/llgoextandtest/llgoext/localitymultipass;internal/locality/...,ssa, andclpass;suite, so native AMD64 CI is authoritative for that architecture.
internal/localityandinternal/locality/layouteach have 100% statementcoverage.
zero uncovered or partial lines. Runtime is a nested module and is exercised
by real-LLGo integration tests.
A full macOS
clrun also completed. Its Go 1.26.5 failures are existingfloating/complex output-golden differences and do not involve locality code.
The ARM64 Ubuntu full package run likewise exposed existing architecture-specific
FileCheck
alloca align 1expectations; focused locality and runtime suites aregreen.
Independent CI follow-up
Ubuntu coverage exposed a pre-existing
runtime.FuncForPCcache publicationrace. A cache entry previously published
pcand*Functhrough separateshared fields, so concurrent replacement could return a function for another
PC. The dedicated final PR commit publishes one immutable
*Funcatomicallyand validates
fn.pc; its stress probe passes on macOS and Ubuntu. This fix isindependent of the locality architecture and remains a separate commit.
Follow-up
This PR is self-contained. #2090 is the GLS-based runtime-state consumer and
will be rebased after this PR is green.