Skip to content

VM micro-optimization series — 8 PRs + reviewer's guide #37

Description

@mparrett

A profile-driven pass over the VM hot paths, sliced into small independent PRs so each can be judged (and reverted) on its own. Every slice is pure pkg/vm (a couple also touch builtin call sites mechanically), with no intended semantics change. This issue is the map + a reviewer's guide so the review time goes to the few decisions that need judgment, not to re-deriving that the green is real.

The PRs

PR Slice What Risk
#29 C — iterators for-of steps built-in iterators with no call / no result object med
#35 D — array-destructure (stacks on #29) [k,v]=arr reads pristine arrays by index med
#30 E — inline strings primitive strings store their header inline; NewString stops allocating med
#31 F — heap sentinel fold heap initialized []bool into a value sentinel (perf-neutral cleanup) low
#32 G — map/set keys Map/Set keys → comparable struct; kills the per-op string/Sprintf alloc low-med
#33 H — sentinel pool recycle the native-reentry sentinel's 1-elem result slice low
#36 I — callback args (stacks on #33) pool callback arg slices (CallArgs2/3/4) across 71 builtin sites med
#34 J — string relational byte comparison for BMP </>/<=/>=, skip the []uint16 materialization low-med

TL;DR

Eight small VM perf PRs. Benchmarks up 30–300% on hot paths; 0 Test262 regressions, measured (not asserted — see below). Each PR is independent and self-contained. Four are low-risk enough to merge on the verification alone; four carry a single design decision worth your eyes. You don't need to read this whole issue — the fast path is enough to act.

Fast path to merge

Trust basis: every commit is build + go vet + full TestScripts green. Every semantics-adjacent slice is additionally diffed against the Test262 language suite as a differential vs a main control — 0 new failures on all of them. Built-ins checked by per-directory sweep — 0 regressions.

Prefer one merge over eight? There's a single branch on the fork, mparrett:perf/vm-microopt-all, with all eight slices stacked in dependency order off main — build + TestScripts + Test262 language all green (0 new failures). Review the pieces here, and if they check out I'll open it as a PR (or you can pull/merge it directly); merging it supersedes the individual PRs. Say the word.

Merge mechanics

Optional: hand a slice to an agent

Validates any slice end-to-end. It needs the Test262 corpus, which the repo pins and fetches:

./setup-test262.sh          # one-time: clones tc39/test262 at the pinned .test262-rev into ./test262

git fetch origin && git checkout <branch>     # e.g. perf/vm-mapset-keys
go build ./... && go test ./tests -run TestScripts

# Test262 language differential vs main (the slices are cut off main):
go build -o /tmp/t262 ./cmd/paserati-test262
./t262 -path ./test262 -subpath language -timeout 0.2s -dump /tmp/slice.dump   # this branch
git checkout main && go build -o /tmp/t262 ./cmd/paserati-test262
./t262 -path ./test262 -subpath language -timeout 0.2s -dump /tmp/base.dump    # control

# new failures (empty output == clean):
comm -12 <(grep '^-' /tmp/slice.dump | sed 's/^.//' | sort) \
         <(grep '^+' /tmp/base.dump  | sed 's/^.//' | sort)

(Dump lines are +path for a pass, -path for a fail; a new failure is a test failing on the slice but passing on the control.)

Per-PR: what to check

PR Check
#29 C Map/Set per-iterator state moved to a PlainObject.internalIterState pointer (+8B) — those slots were formerly enumerable user-visible props, so this makes them spec-correct internal slots. Deopt on a replaced next.
#35 D The Symbol.iterator identity-check that guards the fast path. Instance-level arr[Symbol.iterator] overrides are already ignored by the generic for-of/spread paths — pre-existing gap, filed separately, not introduced here.
#30 E The unsafe.StringData/unsafe.String round-trip and the GC-liveness argument (GC scans the unsafe.Pointer field). -race/checkptr + GOGC=1 stress evidence is in the PR.
#31 F Perf-neutral cleanup. The payload-tagged empty sentinel distinguishing never-set from TDZ. Optional to take.
#32 G SameValueZero canonicalization (NaN via canonical bits, ±0 via bits 0), pointer-identity object keys and why they stay GC-safe, the 32B-vs-16B key-size tradeoff.
#33 H The LIFO free-list invariant (reentries nest strictly) and that put clears the slot for GC hygiene.
#36 I Why pooling is safe: Call copies args into callee registers and NewArguments deep-copies, so nothing retains the slice. One comma-in-string Call deliberately left un-converted.
#34 J Why byte order == UTF-16 order for BMP, and that astral/surrogate content falls back to full UTF-16 so ordering stays correct. Mirrors the existing equality guard.

Notes

  • Numbers: the local deltas (Apple M2) are directional. The perf-label same-runner A/B on each PR is the gate and supersedes them. A few timeout-bound built-ins tests flip to passing because the VM now finishes them inside the harness limit — the count is machine-sensitive, so it's not claimed as a number.
  • Conformance: the audit turned up pre-existing inconsistencies (IsCallable vs IsFunction on AsyncNativeFunction; OpIn's accepted-type list; the array Symbol.iterator instance-override gap). All preserved bit-for-bit — none are changed inside a perf PR; I'll file them separately.
  • A companion arith/dispatch lane (numeric fast paths, IC, dead-code) is staged separately and will follow.
  • My other open PRs (bench-infra, compiler correctness) and the cross-workstream merge order live in the epic tracker Epic: perf benchmarking infrastructure (tracker) #23.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions