feat: add hidden closure environment ABI - #2255
Conversation
LLGo baseline benchmarks
Program measurements
Core language and compiler benchmarks
No main baseline exists yet; all metrics are marked Warning
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Review summary
Reviewed the hidden closure-environment ABI change across the compiler (ssa/, cl/), the C libffi bridge, and the reflect runtime. The design is careful and well-commented; the env-vs-no-env distinction, the zero-sized sentinel, and the reentrancy handling in llgo_ffi_call_with_env (save/restore of previous) all check out for the normal synchronous paths. The docs (doc/closure.md) match the implementation.
A few concrete findings are left as inline comments. Cross-cutting notes that don't map to a single line:
Performance (worth a benchmark vs. the base commit):
runtime/internal/clite/ffi/_wrap/libffi.c— theffi_call_gofast path is only weakly linked and is disabled on Apple targets (LLGO_FFI_GO_ABI_MATCHESexcludes__APPLE__). Per the code's own comment, common system libffi builds omit that symbol, so on typical Linux and all macOS the slow trampoline is the default path for every env-bearing FFI call. That path spills/reloads the whole argument register file, marshals arguments twice, and adds an extra out-of-line call. Sincereflect.Value.callnow routes throughffi.CallWithEnv, this is on the reflect hot path — worth measuring the regression.runtime/internal/lib/reflect/makefunc.gomakeMethodValue— each method value now allocates a libffi closure (ffi_closure_alloc, mmap-backed executable memory) plus a heap funcval and an extra indirection per call, versus the previous direct funcval. If method values are created in loops this is a real allocation/perf cost; consider caching per(type, method).
Memory-safety (low severity, documentation-only asks):
runtime/internal/clite/ffi/_wrap/libffi.c— the aarch64 Apple/Android and arm32 trampolines stash callee-saved registers/return address in the single_Thread_localcontext and restore them only after the real target returns normally (matching thepreviousrestore inllgo_ffi_call_with_env). A non-local exit that unwinds across theffi_callframe (longjmp-style) would skip both restores, leaving callee-saved registers clobbered and the TLS record stale. Synchronous nesting and balanced signal-handler nesting are safe. Recommend a comment asserting the "target must return normally / every entry goes through the balanced save/restore" invariant next to the struct definition.runtime/internal/lib/reflect/makefunc.go:74—keepAlivestores the interior pointer&closure.Fnto retain the whole*ffi.Closure. This relies on interior-pointer retention in the current GC; storingclosuredirectly would be more robust. No live bug.
a009e26 to
5b379e0
Compare
|
Review follow-up is pushed on top of current
I also folded the independently reproduced The review performance concern already has same-machine A/B data in the PR body. Caching reflect method-value closures is a separate allocation/lifetime design change and is intentionally not included here. |
| } | ||
|
|
||
| func makeFunc(typ Type, method bool, fn func(args []Value) (results []Value)) Value { | ||
| func makeFunc(typ Type, fn func(args []Value) (results []Value)) Value { |
There was a problem hiding this comment.
这里不再需要 makeFunc 函数,可以合并到 MakeFunc 函数。
Summary
llssa.Functionnestorswiftselfon validated native targets; Wasm and portable fallbacks use exact typed env/no-env edges__llgo_stubcall layersret, callback-wrapper, and C ABI loweringCallWithEnvbridge; no libffi rebuild is required{fn, env}with nil meaning no physical env, while eliding provably zero-sized lexical environmentsBenefits
range4.gonamed-yield case valid on Go 1.24 through 1.26 and removes its Darwin xfailReview follow-up
//llgo:envand// llgo:envthrough the shared directive parser0x48remains reserved for the coroutine backendnestt2/x7 transport and its match with the libffi bridge; ESP32-C3 exercises the riscv32 pathValidation
Fork validation is green in cpunion/llgo#96: 39 successful checks on original implementation commit
eca0053a2997.Coverage includes Linux amd64/arm64, macOS arm64/Intel, Go 1.24/1.26, Wasm explicit transport,
nest,swiftself, O0/O2/LTO, plain Go and C funcvals, captured and zero-sized closures, nil and typed-nil method values, direct interface calls, reflection/libffi, variadics, aggregates, andsret. The existing ESP32/ESP32-C3 suites pass; the new full-reflect runtime test is precisely skipped there because the same test does not compile on the baseline embedded runtime.Additional validation after rebasing onto current main:
test/range4.gowith an empty xfail set on Go 1.24.11, 1.25.7, and 1.26.5git diff --check upstream/main...HEADSame-machine A/B on Apple M4 Max measured plain dynamic funcval calls improving from about 1.027 ns/op to 0.770 ns/op, captured closures remaining flat, and representative executable sizes decreasing.
Supersedes #2248, whose closed head cannot be reopened after the fork branch was force-pushed.
Closes #2170