Conversation
`check_op` and `check_tyd` walk the whole body of every global operator and type declaration to check that it only depends on things a global item may depend on. Outside a section that walk can never fail: `local` and `declare` items are rejected outside sections, so everything bound in the environment is `Global` there. `check_ax`, `check_modtype` and `check_module` already guard their `Global` branch with `scenv.sc_insec`; do the same for the two that were left out. The walk is proportional to the size of the term, so it costs little on hand-written developments and a lot on generated ones. On a file shaped like a Jasmin extraction of constant tables -- 20 `op` bodies of 5000 word literals each, ~1.9MB of source -- `require`ing the theory goes from 10.85s to 9.09s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fkSDht523sZweUb7G7yR6
`select_op_outcomes` rebuilt `tfun_expected` inside the per-candidate loop, so each candidate allocated its own throw-away unification variable for the return type (plus the arrow type around it) even though the expected type is the same for all of them. Build it once, in a scratch unienv that every candidate is then forked from: the candidates stay independent, since `UniEnv.copy` still gives each one its own union-find. Also short-circuit `UniEnv.openty_r` when there is no type parameter to open and no explicit type argument: the substitution is the identity and there is no instance to report. A non-`None` `tvi` keeps the general path, which reports an arity mismatch rather than silently ignoring it. Overloaded symbols are resolved once per occurrence, which adds up on generated code. On 20 constant tables of 5000 negative word literals -- Jasmin extracts bytes signed, as `W8.of_int (-120)` -- `select_op` examines 1161954 candidates, and unary minus accounts for 900584 of them: it has nine definitions in scope once the word theories are imported, and eight are discarded at every literal. `require`ing the theory goes from 9.09s to 8.74s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fkSDht523sZweUb7G7yR6
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 independent savings on the cost of
require-ing a largemachine-generated theory. No user-visible behaviour change.
Reproducing
Jasmin extractions of constant tables are the workload that made this
visible: a few very large
opbodies, whose elements are negativeword literals (Jasmin extracts bytes signed). This generates a file
of that shape — 20 tables of 5000 elements, ~1.9MB:
Going through a
requirematters:.ecorecords digests only, so arequired theory is re-parsed and re-elaborated in full on every run —
only its proofs are skipped. (Passing
tables.ectocompiledirectly would be skipped outright once its
.ecois valid, andmeasure nothing.)
CPU time, minimum of 16 runs split over the two orderings of the three
binaries, on a machine that was not quiet:
main[section][unify]The section commit is ahead in 16 rounds out of 16, the unify commit
in 13 out of 16.
[section] skip the global dependency walk outside sectionscheck_opandcheck_tydwalk the whole body of every globaloperator and type declaration, to check it only depends on things a
global item may depend on. Outside a section that walk can never
fail:
localanddeclareitems are rejected outside sections, soeverything bound in the environment is
Globalthere.check_ax,check_modtypeandcheck_modulealready guard theirGlobalbranch withscenv.sc_insec; this adds the same guard to thetwo that were left out.
The walk is proportional to the size of the term, so it is cheap on
hand-written developments and expensive on generated ones. Requiring
the
Array*/WArray*/BArray*theories a Jasmin extraction emitsgets about 20% faster on its own.
[unify] build the expected type once per overloading queryselect_op_outcomesrebuilttfun_expectedinside the per-candidateloop, so each candidate allocated its own throw-away unification
variable for the return type (plus the arrow type around it), even
though the expected type is the same for all of them. It is now built
once, in a scratch unienv that every candidate is forked from — the
candidates stay independent, since
UniEnv.copystill gives each oneits own union-find.
UniEnv.openty_ralso short-circuits when thereis no type parameter to open and no explicit type argument; a
non-
Nonetvikeeps the general path, which reports an aritymismatch rather than silently ignoring it.
What makes this worth doing is that overloaded symbols are resolved
per occurrence. Counting the candidates
select_opexamines on thefile above:
Unary minus alone accounts for 78% of them: it has nine definitions in
scope once the word theories are imported, and eight are discarded at
every one of the 100k negative literals.
Testing
make unit101/101,make stdlib128/128.Deliberately not included
A more aggressive
select_oppre-filter, rejecting a candidatewhose rigid argument head already contradicts its domain before any
of the unification machinery. Worth only about 2 further points, for
~30 lines and a soundness argument about type abbreviations.
Generalising the section guard from
sc_insecto "has this sectionintroduced any local or declared item". That is the more accurate
statement of the invariant and subsumes the guard above, but
removing all section checks is worth only 2.5% on the most
section-heavy stdlib theories (
PROM,ROM,Hybrid,Strong_RP_RF,SplitRO,PRP,Birthday,LoopTransform,DLog,PKE,Indist, required as dependencies: 2.40s -> 2.34s),and stdlib sections open on their
declares anyway, so the guardwould almost never fire. It also has a trap worth recording: a flag
stored in
scenvis dropped byexit_theory, which returnsoget scenv.sc_top, sowould stop being rejected.
The structural cost is untouched and is larger than either of these:
.ecois a digest cache, not a compiled theory, so a required theoryis re-parsed and re-elaborated from source on every
require, inevery session — for a file with no proof at all, the
.ecosavesnothing.
🤖 Generated with Claude Code
https://claude.ai/code/session_016fkSDht523sZweUb7G7yR6