Stop 1+, 1-, logtest and isqrt consing on every call - #1817
Open
dg1sbg wants to merge 2 commits into
Open
Conversation
The n-ary CL bit and arithmetic functions are bound to C++ entry points that take their arguments as a list -- cl__logand(List_sp) at src/core/bits.cc:830, cl__logior(List_sp) at :843, cl__max(Real_sp, List_sp) at numbers.cc:190 -- so every full call to one conses one cons per argument. numlib.lisp avoids that by defining compiler macros that rewrite (logand a b) to core:logand-2op, (- a b) to core:two-arg--, and so on. For those rewrites to apply, the macros have to be in effect before the definitions that use them are compiled, which is what the KLUDGE comment at the top of the file is there to explain. 379bb29 consolidated the compiler macros into a single block and placed it after the seventeen definitions that open numlib.lisp, moving the 1+ and 1- macros later rather than earlier and leaving that KLUDGE comment stranded above an empty (eval-when (:compile-toplevel)). All seventeen definitions consequently baked in a full call to an n-ary function. Move the block back above them, to the position the orphaned comment already marks, and fold the empty stub into it. This is a pure relocation: no line of the block is altered, and the only lines removed are the empty eval-when and three comment lines that duplicated the KLUDGE comment. Measured on boehmprecise, allocation per full call, before -> after: cl:1+ 48 B -> 0 B cl:1- 24 B -> 0 B cl:logtest 48 B -> 0 B cl:isqrt 48 B -> 0 B In the 33-row allocation-constant probe the logtest row goes from 48.00 to 0.00 B/call and no other row changes.
(- a b) expanded to (core:two-arg-- a (+ b)), and (/ a b) to (core:two-arg-/ a (* b)). A one-argument + or * is not free: expand-associative renders it as (the-single number x), and the bytecode compiler emits that as real calls to %the-single and values. So every two-argument subtraction and division in bytecode-compiled code made three calls where one would do: called-fdefinition CORE:TWO-ARG-- ref 0 called-fdefinition CORE::%THE-SINGLE const 'NUMBER called-fdefinition VALUES ref 1 call-receive-one 1 call-receive-one 2 call 2 The check is redundant, by expand-associative's own comment: it is needed only in the one-argument case because "with more arguments, the two-arg-fun will do checks". core:two-arg-- and core:two-arg-/ do exactly that, and an argument form is already in a single-value context, so the (values ...) wrapper buys nothing either. Cleavir already transforms %the-single into a THE (cleavir/transform.lisp:375), so this only ever cost the bytecode path -- which is the path this configuration compiles by default. Add core:expand-inverse for the shape - and / share, mirroring core:expand-associative, and use it for both the build-time compiler macros in numlib.lisp and the load-time ones in cmp/opt/opt-number.lisp. As upstream already does for expand-associative, cross-clasp carries its own copy in macrology.lisp plus entries in packages.lisp and base.lisp. Two-argument - and / now emit a single call. Behaviour is unchanged: the full before/after output of -, / at one, two and three arguments over integers, ratios, bignums, double-floats and complexes is identical, extra values are still truncated, and a non-number argument still signals TYPE-ERROR with datum and expected-type NUMBER -- the same condition %the-single raised.
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.
Two small, independent fixes to the numeric compiler macros. The first is a regression fix.
1.
numlib.lispdefines its compiler macros after the definitions that need themThe n-ary CL bit and arithmetic functions are bound to C++ entry points that take their arguments as a list —
cl__logand(List_sp)(src/core/bits.cc:830),cl__logior(List_sp)(:843),cl__max(Real_sp, List_sp)(src/core/numbers.cc:190) — so every full call to one conses one cons per argument. Measured onboehmprecise, two arguments:cl:+,cl:*,cl:=,logand,logior,logxorcl:-,cl:maxcore:two-arg-+(the 2-op reference)numlib.lispneutralises this with compiler macros rewriting(logand a b)tocore:logand-2opand so on — but they only help code compiled after them.379bb29 consolidated those macros into a single block placed after the seventeen definitions that open
numlib.lisp. That moved the1+/1-macros later rather than earlier, and left theKLUDGEcomment explaining the placement stranded above an empty(eval-when (:compile-toplevel)). All seventeen definitions consequently baked in a full call to an n-ary function.Moving the block back above them — to the position the orphaned comment already marks:
cl:1+cl:1-cl:logtestcl:isqrtThis commit is a pure relocation: a multiset diff of the file's lines shows zero lines added, and five removed — the empty
eval-whenplus three comment lines that duplicated theKLUDGEcomment.2. Two-argument
-and/carry a redundant type check(- a b)expanded to(core:two-arg-- a (+ b)), and(/ a b)to(core:two-arg-/ a (* b)). A one-argument+or*is not free:expand-associativerenders it as(the-single number x), which the bytecode compiler emits as real calls to%the-singleandvalues. So every two-argument subtraction and division in bytecode-compiled code made three calls where one would do:The check is redundant by
expand-associative's own comment — it is needed only in the one-argument case, because "with more arguments, the two-arg-fun will do checks".core:two-arg--andcore:two-arg-/do exactly that, and an argument form is already in a single-value context, so the(values ...)wrapper buys nothing either. Cleavir already transforms%the-singleinto aTHE(cleavir/transform.lisp:375), so this only ever cost the bytecode path.core:expand-inversecaptures the shape-and/share, mirroringcore:expand-associative, and is used for both the build-time compiler macros innumlib.lispand the load-time ones incmp/opt/opt-number.lisp. As is already done forexpand-associative, cross-clasp carries its own copy inmacrology.lispplus entries inpackages.lispandbase.lisp.Both two-argument forms now emit a single call.
Testing
boehmprecise, macOS arm64,:build-mode :native,:extensions ():logtestrow moves (48.00 → 0.00 B/call). Every other row is byte-identical, including after the second commit.logteston bignums, negatives, zero and mixed fixnum/bignum plus an exhaustive-40..40cross-check against its definition;isqrtinvariants over0..2000;1+/1-across the fixnum/bignum boundary and on ratios, doubles and complexes;ffloor/fceiling/ftruncate/froundincluding two-argument forms;phase/cis; the real and complex branches ofasinh/acosh/atanh/asin/acos; and n-ary arity and identity checks for+ - * / logand logior logxor logeqv min max < = /=. 0 failures.-and/semantics: the full before/after output at one, two and three arguments over integers, ratios, bignums, double-floats and complexes is byte-identical; extra values are still truncated ((- 5 (values 1 2))= 4); and a non-number argument still signalsTYPE-ERRORwithdatumandexpected-type NUMBER— the same condition%the-singleraised.ninja -C build test: 1979 successes, 4 failures, all four in*expected-failures*, 0 unexpected — identical before and after.ninja -C build ansi-test: 27 failures, 0 unexpected, out of 21936.Verified on a tree that also carries several unrelated merged PR branches; all six files this PR touches are byte-identical between that tree and
main, so what was built and tested is exactly the content here.CI note
PRINT.DOUBLE-FLOAT.RANDOMis flaky for reasons unrelated to this PR — see #1816. It draws random doubles and trips a long-standing printer bug at roughly 1 in 13000, which reproduces identically on a 2.7.0 binary. If CI shows it red, it is not this change; the rest of the failure list should be compared.