Stop allocating a scratch vector for every bytecode &key call - #1820
Open
dg1sbg wants to merge 1 commit into
Open
Stop allocating a scratch vector for every bytecode &key call#1820dg1sbg wants to merge 1 commit into
dg1sbg wants to merge 1 commit into
Conversation
parse-key-args built a SimpleVector to collect keyword arguments, then immediately pushed its contents onto the VM stack and dropped it. The vector never outlived the opcode, and it was allocated unconditionally -- before the lcc_nargs > more_start guard -- so a call passing no keywords at all still paid for it. The cost was 24 + 8n bytes for a callee with n &key parameters, charged per call: (defun k1 (a &key x) ...) called (k1 1) 32 B (defun k2 (a &key x y) ...) called (k2 1) 40 B (defun k3 (a &key x y z) ...) called (k3 1) 48 B (defun k6 (a &key p q r s u v)) called (k6 1) 72 B (string= "abc" "abd") 56 B All of those are 0 B now. The parameter slots are the destination anyway, so push them first and write each argument directly into its slot. The index identity is exact rather than assumed: the loop being deleted pushed key_id descending, so key_id 0 ended on top, and stackref is documented in-tree as "0 is most recently pushed" -- hence slot n is key n. Ordering: the odd-argument-count check is hoisted above the pushes so that error path cannot see them. The unrecognized-keyword throw still can, which is safe -- the VM stack is a registered GC root (allocateRootsAndZero, gctools/threadlocal.cc), sp is a plain C local, and both ~VMFrameDynEnv_O and VMFrameDynEnv_O::proceed restore _stackPointer on unwind. Slots are initialized to unbound before anything can collect. Peak stack use is unchanged: exactly key_count slots either way, earlier. The long-operand form gets the same treatment for consistency. It takes more than 127 &key parameters to reach, since key_count_info is (key_count << 1) | aokp. Adds regression tests for slot identity across argument permutations, leftmost-wins on duplicate keywords, &allow-other-keys, and 16 parameter slots live across a collection. A reversed or off-by-one slot index binds the wrong parameter with no error at all, so these pin the identity rather than the allocation. Gates: boehm 1984, boehmprecise 1986, both with no unexpected failures; ANSI 21936 tests with no unexpected failures.
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.
parse-key-argsin the bytecode VM builds aSimpleVectorto collect keyword arguments, then immediately pushes its contents onto the VM stack and drops it. The vector never outlives the opcode. It is also allocated unconditionally — before thelcc_nargs > more_startguard — so a call that passes no keywords at all still pays for it.The cost is 24 + 8n bytes per call for a callee with n
&keyparameters, charged whether or not the caller passes any keywords:(defun k1 (a &key x) ...)(k1 1)(defun k2 (a &key x y) ...)(k2 1)(defun k3 (a &key x y z) ...)(k3 1)(defun k6 (a &key p q r s u v) ...)(k6 1)(defun kopt (a &optional o &key x y) ...)(kopt 1)string=(string= "abc" "abd")Measured with
gctools:bytes-allocatedover 200000 warmed calls.The change
The parameter slots are the destination anyway, so push them first and write each argument directly into its slot.
The index identity is derived rather than assumed: the loop being deleted pushed
key_iddescending, sokey_id 0ended up on top, andstackrefis documented inthreadlocal.has "0 is most recently pushed" — hence slot n is key n. This matches the comment atcleavir/compile-bytecode.lisp:879, "the leftmost key is the last pushed".The odd-argument-count check is hoisted above the pushes so that error path cannot observe them. The unrecognized-keyword throw still can, which is safe: the VM stack is a registered GC root (
allocateRootsAndZero,gctools/threadlocal.cc),spis a plain C local rather thanvm._stackPointer, and both~VMFrameDynEnv_OandVMFrameDynEnv_O::proceedrestore_stackPointeron unwind. Slots are initialized tounboundbefore anything that can collect runs.Peak stack use is unchanged — exactly
key_countslots are pushed either way, just earlier.The long-operand form at the second
parse_key_argscase gets the same treatment for consistency. It takes more than 127&keyparameters to reach, sincekey_count_infois(key_count << 1) | aokp.Tests
A reversed or off-by-one slot index would bind the wrong parameter with no error at all, so the new tests pin the slot identity rather than the allocation:
&allow-other-keysin the lambda list and as a runtime argument, plus the unknown-keyword and odd-argument error pathsVerification
Built from scratch on this branch (
koga+ninja), no local modifications to any dependency:test-boehm— 1965 successes, no unexpected failurestest-boehmprecise— 1967 successes, no unexpected failuresansi-test-boehm— 21936 tests, 27 failures, no unexpected failuresmacOS arm64, LLVM 22.1.8.