Add evaluator fast paths: cached field plans, reflect-space index chains, typed binary ops#26
Merged
Merged
Conversation
…ins, typed binary ops Four per-Run optimizations, validated against the existing bench suite (baseline -> after, Linux/amd64, go1.26): - Cache Go-name field plans for untagged struct lookups instead of calling reflect's FieldByName per access (it re-walks the field set every time and dominated struct-env CPU profiles at ~30%). envStruct 145ns -> 84ns, structTagsWide/go_names 559ns -> 202ns, largeStructAccess 310ns -> 220ns. - Walk ident-rooted index/selector chains (a[0][1], a.b[i].c) in reflect space, materializing only the leaf. The general path boxes every intermediate through any, copying aggregate data: indexing a struct field holding a 10 MiB array memcpy'd the array per access. largeNestedArrayAccess 1.52ms / 10.5MB per op -> 167ns / 65B per op. - Dispatch exact int64/int/float64/string/bool operand pairs in applyBinary through typed helpers, skipping the reflect-based coercion funnel (asString called reflect.ValueOf on every non-string operand). Integer equality keeps looseEqual's float64 comparison so results are bit-for-bit identical. filter 90us -> 70us, envMap 101ns -> 68ns. - Stream higher-order forms through an itemSeq view instead of materializing typed slices into []any before iteration, and hoist the error reflect.Type to a package var in finishCall/prepareFunc. filter 40,840B -> 24,456B per op, filterMap 4,752B -> 2,960B per op. Untagged field lookup now reuses the same VisibleFields-based plan as tagged lookup: ambiguous promoted names resolve to "not found" (same as FieldByName), and traversing an embedded nil pointer returns an ErrEvaluate instead of panicking. New index-chain tests pin the fast path to the general path's values and error messages. https://claude.ai/code/session_018MkLtLrR1u8FEo1LgxyhrN
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.
Summary
Four per-Run optimizations to the evaluator hot path, driven by CPU/alloc profiling of the existing bench suite. No language behavior changes; error messages and result values are preserved (pinned by new tests).
1. Cached field-index plans for untagged struct lookups (
struct_tags.go)reflect.Value.FieldByNamere-walks the field set (including embedded promotion) on every call and accounted for ~30% of cumulative CPU in struct-env profiles. Untagged lookups now reuse the sameVisibleFields-based plan machinery as tagged lookups, cached per type in a package-levelsync.Map, so repeated lookups become a map hit +FieldByIndex.2. Reflect-space fast path for index chains (
program.go)Ident-rooted chains of index/selector hops (
a[0][1],a.b[i].c) are now walked in reflect space, materializing only the leaf — mirroring the existingevalSelectorChainRVfast path. The general path boxes every intermediate throughany, which copies aggregate data:Data[0][0]on a struct holding a 10 MiB array memcpy'd the array on every access. Single hops on map envs still take the general path (values there are already boxed, so reflect adds only overhead).3. Typed fast path in
applyBinary(program.go)Exact
int64/int/float64/string/booloperand pairs — the overwhelmingly common case — dispatch through typed helpers, skipping the reflect-based coercion funnel (asStringcalledreflect.ValueOfon every non-string operand, twice per binary op). Integer==/!=intentionally keepslooseEqual's float64 comparison so results are bit-for-bit identical to the general path. Unsupported operators fall through so error messages keep the original operand types.4. Streaming higher-order forms + minor call-path cleanup (
higher_order.go,reflect.go,prepared.go)iterItemsno longer materializes typed slices into a[]anybefore iteration; forms iterate anitemSeqview that boxes elements lazily. Also hoists theerrorreflect.Type to a package var instead of rebuilding it infinishCallon every call.Benchmarks
Linux/amd64, go1.26,
-benchtime 1s(baseline → this PR):envStructstructTagsNested/go_namesstructTagsWide/go_namesenvMaplargeStructAccesslargeNestedStructAccesslargeNestedArrayAccessfilterfilterMapfilterFirstcallFieldcallMethodexprarrayIndexBehavior notes
reflect.Value.FieldByName(previously also not found, via a different route).ErrEvaluate("cannot access … on nil pointer") instead of panicking inside reflect — same message the tagged path already produced.Testing
-race.index_chain_test.gopins the index-chain fast path to the general path's values and error messages, including map-env/struct-env parity.https://claude.ai/code/session_018MkLtLrR1u8FEo1LgxyhrN
Generated by Claude Code