Skip to content

meta: emit and cache package semantic summaries#2061

Open
luoliwoshang wants to merge 81 commits into
xgo-dev:mainfrom
luoliwoshang:wip/meta-emit
Open

meta: emit and cache package semantic summaries#2061
luoliwoshang wants to merge 81 commits into
xgo-dev:mainfrom
luoliwoshang:wip/meta-emit

Conversation

@luoliwoshang

@luoliwoshang luoliwoshang commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduce package-level semantic metadata production, persistent cache storage, and a whole-program read-only view for future link-time dead-code analysis.

This PR implements the producer and data path:

  • emit ordinary symbol reachability and function-level semantic demands from CL/SSA
  • emit type children, concrete method slots, and structural interface method signatures
  • serialize each package summary into a versioned, mmap-friendly .meta file
  • write metadata on cache misses and mmap it on cache hits
  • merge package-local symbol namespaces into a lazy GlobalSummary

Related design proposal: #1853

Scope and data flow

CL/SSA emission
  -> meta.Builder
  -> PackageMeta
  -> .meta build-cache entry
  -> cache-hit mmap loading
  -> GlobalSummary

The emitted facts are:

  • OrdinaryEdges: ordinary calls and symbol references extracted from LLVM IR
  • FuncDemand: interface conversion, interface method call, named reflection method, and dynamic reflection method demands
  • TypeChildren: direct component types needed for type reachability
  • MethodInfo: concrete method name/signature plus IFn and TFn symbols
  • InterfaceInfo: structural interface method names and signatures

The existing dead-code-drop configuration currently gates whether these summaries are collected and required in cache entries. It does not make this PR responsible for deciding what is dead or rewriting linked IR.

Cache miss path

  1. Cache use is bypassed when caching is disabled, the package is main, -a/force rebuild is active, or no package fingerprint is available.
  2. For a package that was not loaded from cache, metadata collection is requested only when the dead-code-drop configuration is enabled.
  3. cl.NewPackageExWithEmbedMeta creates a package-local meta.Builder only when metadata is requested.
  4. CL/SSA records semantic facts while compiling. At package completion, FinishMetaCollection extracts ordinary LLVM symbol edges, builds the immutable PackageMeta, and attaches it to the compiled package.
  5. Non-main packages are saved to the build cache. The archive is stored as usual; when metadata is enabled, PackageMeta.WriteTo writes a temporary .meta file which is atomically published with rename; the manifest is stored separately.

Cache hit path

  1. The archive and manifest must exist and the manifest must parse successfully.
  2. When metadata is enabled, the matching .meta file is mandatory. meta.Open validates its header and sections and maps it read-only; a missing or invalid file turns the lookup into a cache miss.
  3. When metadata is disabled, the .meta file is neither required nor opened, and the package's Meta remains nil. Existing archive-only cache entries therefore remain usable.
  4. A successful hit restores the archive path, linker arguments, runtime flags, and mapped PackageMeta, then marks the package as a cache hit. Compilation still registers package types, but does not rebuild metadata or regenerate package code.

The cache validation helper follows the same rule: archive and manifest are always required, while .meta is opened and closed for validation only when metadata is required.

Version 1 wire format

The file uses magic LLPM, version 1, little-endian uint32 values, and 4-byte-aligned sections.

Header (36 bytes)
  [0:4]   magic = "LLPM"
  [4:8]   version = 1
  [8:36]  seven absolute section offsets, in order:
            StringTable
            Symbols
            OrdinaryEdges
            FuncDemand
            TypeChildren
            MethodInfo
            InterfaceInfo

StringTable
  starts at byte 36
  concatenated string bytes
  zero padding to a 4-byte boundary

Symbols
  nsyms
  records[nsyms] = {nameOff, nameLen, reserved}          // 12 bytes

OrdinaryEdges: CSR<Symbol>
  nsyms
  offsets[nsyms+1]
  data[] = Symbol                                        // 4 bytes

FuncDemand: CSR<localFuncDemand>
  nsyms
  offsets[nsyms+1]
  data[] = {kind, target, extra}                         // 12 bytes

TypeChildren: CSR<Symbol>
  nsyms
  offsets[nsyms+1]
  data[] = Symbol                                        // 4 bytes

MethodInfo: CSR<localMethodSlot>
  nsyms
  offsets[nsyms+1]
  data[] = {nameOff, nameLen, mtype, ifn, tfn}           // 20 bytes

InterfaceInfo: CSR<localMethodSig>
  nsyms
  offsets[nsyms+1]
  data[] = {nameOff, nameLen, mtype}                     // 12 bytes

Header section offsets are absolute byte offsets. CSR offsets are record indexes into that section's flat data array, not byte offsets. Every CSR nsyms equals the Symbols count. Symbol values index the package-local Symbols section. Name offsets are relative to StringTable, name lengths exclude alignment padding, and the Symbols reserved field is written as zero. The final InterfaceInfo section extends to EOF.

FuncDemand.kind values are:

Value Kind Local payload
1 DemandUseIface target is a type symbol
2 DemandIfaceMethod target is an interface symbol; extra is its method index
3 DemandNamedMethod target is a StringTable offset; extra is the string length
4 DemandReflectMethod no additional payload

GlobalSummary

NewGlobalSummary interns every package-local symbol name into one global Symbol namespace and builds a locToGlb translation table for each package. Method names use a separate Name namespace.

For duplicate symbols, the summary selects one owner and prefers packages carrying semantic function, method, interface, or type facts over ordinary-edge-only copies. Public queries lazily translate the selected owner's package-local data into global IDs:

  • OrdinaryEdges
  • FuncDemands
  • TypeChildren
  • MethodSlots
  • IfaceMethods

GlobalSummary borrows its input PackageMeta values. Mmap-backed package summaries must therefore remain open for the lifetime of the global summary and any aliased query results.

Tests

  • metadata builder, binary validation, mmap lifetime, formatting, and global translation tests in internal/meta
  • cache behavior for metadata-enabled and metadata-disabled entries in internal/build
  • focused SSA/CL emission tests for type, method, interface, and reflection facts
  • _testmeta golden tests covering emitted package summaries

Out of scope

This PR intentionally does not include the metadata consumer or mutate linked output. The following work has been removed and belongs in follow-up PRs:

  • dead-code reachability decisions
  • DCE pass integration
  • LLVM global/type initializer override rewriting
  • link-time dead-code override application
  • _testdrop symbol-retention and drop regression cases

luoliwoshang and others added 30 commits May 20, 2026 15:14
# Conflicts:
#	internal/build/build.go
#	ssa/abitype.go
… format

Introduces internal/meta, a new binary format and read/write layer for
LLGo's whole-program DCE package summary cache (.meta files).

Key design decisions:
- Zero-copy reads via mmap: PackageMeta is a thin view over a mmap'd byte
  slice; Edges, TypeChildren, MethodSlots and IfaceMethods all use
  unsafe.Slice to reinterpret the on-disk bytes directly, with no alloc.
- CSR layout (Compressed Sparse Row): each per-symbol section stores an
  offsets[nsyms+1] prefix array and a flat data array, matching the design
  used by Go's own goobj format (BlkRelocIdx + BlkReloc).
- Single allocation on write: Builder.Build() pre-computes all section sizes
  and offsets in one pass, allocates one []byte, then writes every section
  directly at its target offset — no intermediate buffers, no seek-back.
- Lazy remap in GlobalSummary: merging N PackageMetas only interns the
  string tables (O(unique_symbols)); edges and type-children are translated
  lazily on query, avoiding the O(total_edges) rewrite cost of the MVP.
- NameRef for method names: method short names are stored as {Off, Len}
  pairs referencing the string table, keeping them distinct from module-level
  symbols (LocalSymbol) while still enabling cross-package matching via
  global Name interning in GlobalSummary.
- Compile-time layout assertions: const expressions verify sizeof(Edge)==12,
  sizeof(MethodSlot)==20, sizeof(MethodSig)==12 so any struct drift that
  would break the zero-copy reinterpretation fails at build time.

Sections in the .meta wire format:
  stringTable | Symbols | Edges (CSR) | TypeChildren (CSR) |
  MethodInfo (CSR) | InterfaceInfo (CSR) | ReflectBitmap

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the MVP's uvarint-based metadata format with the new mmap-based
zero-copy format (internal/meta). All read paths use unsafe reinterpretation
of on-disk bytes; write paths use single-allocation serialization.

Key changes:
- ssa/cl: switch metaBuilder from *metadata.Builder to *meta.Builder;
  AddIfaceMethod/AddTypeChild now deduplicate internally
- internal/build: wire cache miss/hit paths to meta.ReadMeta/Build/WriteMeta
- internal/deadcode: analyze.go uses meta.GlobalSummary with GMethodSlot
  (flat struct, no nested .Sig)
- internal/meta: add zero-copy SymbolName/NameString via unsafe.String,
  add typeChildrenSet map for O(1) dedup, add FormatMeta for golden tests
- cl/_testmeta: update 9 golden files to new deterministic sort order

Perf (etcd client demo, 334 packages):
  summary merge: 114.7ms → ~23ms  (5x faster via lazy remap + zero-copy strings)
  DCE analyze:   ~34ms unchanged
  total:         ~148ms → ~52ms   (2.8x faster)

Co-Authored-By: Claude <noreply@anthropic.com>
Remove Phase 2 eager translation — instead of scanning every type in all
334 packages and translating all ~10080 method slots up front, only mark
concrete/interface type kinds in Phase 1 (cheap CSR range checks, no slot
translation). MethodSlots() and InterfaceMethods() now translate lazily on
first access and cache the result.

DCE typically reaches only ~500 of ~10000+ types, so lazy translation
avoids thousands of unnecessary pm.NameString + internName calls.

Perf (etcd client demo, 334 packages):
  merge: 115ms → ~18ms (6.4x faster)
  total: ~149ms → ~48ms (3.1x faster)

Co-Authored-By: Claude <noreply@anthropic.com>
PackageMeta query methods are now all lowercase (private). External code
interacts only through GlobalSummary, Builder, ReadMeta, Bytes, and Close.

- Drop IsConcreteType, IsInterface, IsCompositeType — callers use
  nmethodSlot/ntypeChild/nifaceMethod count methods instead
- Add N-prefix count methods (Go goobj convention): nedge, ntypeChild,
  nmethodSlot, nifaceMethod — single CSR range check, no unsafe pointer
- All CSR accessors share csrSlice[T] generic helper, eliminating
  repeated CSR+unsafe pointer boilerplate
- Remove NSyms() method — same-package callers use pm.nsyms field
- Remove NewPackageMetaFromBytes — no external callers
- Make buildMethodImplKeys fully lazy: concrete type methodImplKeys
  are computed on-demand in flood's usedInIface path; only methodRefs
  reverse index is built eagerly
- MetaString → String() (fmt.Stringer) — zero additional public API
- Drop ConcreteTypes() from GlobalSummary; isConcrete tracking removed
  from Phase 1

Co-Authored-By: Claude <noreply@anthropic.com>
# Conflicts:
#	chore/gentests/gentests.go
#	cl/compile_test.go
#	internal/build/collect_test.go
#	ssa/expr.go
#	ssa/interface.go
@luoliwoshang
luoliwoshang marked this pull request as ready for review July 11, 2026 12:22
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