Repository navigation
verify: Share the subterms of logic terms instead of copying them - #64
Merged
Merged
Conversation
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.
Every copy of a Layer-3 term copied the whole term, and a module is mostly copies. Each obligation's goal states every assumption before it,
!A1 || (!A2 || ... || C), cloned from the passive program; each obligation's query clones its goal under a negation, the complete goal clones every goal, and the complete query clones the complete goal. So a module's memory grows with its obligations times what comes before them, four times over, and the simplifier rebuilds the complete goal while the old one is alive.The ULEB128 case study (
suite/llvm_uleb128.cpp,--jobs=8in its RUN line) is where that stops CI. Onmainit peaks at 14.9 GB (ru_maxrss, 4 cores), and the Linux runners have 16 GB beside the three other lit workers. In a run of the Verify suite in lit's order on 4 workers, the whole run's anonymous memory peaked at 16.4 GB: this test at 14.6 GB while the certifier oflean_semantics.cppheld 1.7 GB. That is theThe runner has received a shutdown signal(exit 143) of the Linux release jobs:mainafter #61 (run 39956700035), #62 twice, and #63's own release job (run 39993647759), 18 to 28 minutes intocheck-clang, whatever else each fixed. A heap profile of lowering it (valgrind massif,--lower-only, one job) puts 11.7 of its 12.1 GB peak incloneVCExpr.It grew in #57:
--lower-onlytook 8.2 GB on #57's base and 12.7 GB after it, since the definedness checks of contract clauses gaveencode_uleb12860 moreboundsobligations (64 instead of 4): one for thebuffer[k]each of its 20 postconditions reads, and two (on entry and preserved) for each of its 20 loop invariants. Every one repeats the whole path before it.Design
Terms share their subterms, which nothing changes once shared, as Verus's AIR shares
Arc<ExprX>, Frama-C's Qed hash-conses its terms, and Lean sharesExpr:LogicExpr::ChildrenandPatternsareLogicExprRef,llvm::IntrusiveRefCntPtr<const LogicExpr>(LLVM's intrusive count, 8-byte pointers, no control block). A term's root is still owned (std::unique_ptr<LogicExpr>) and may be changed; its subterms areconst, so the compiler rejects any change to a node another term may share. Every root field and every reader stays as it was.cloneLogicExprcopies the root's fields and shares its children. A goal then states its assumptions, its query negates it, and the complete goal conjoins the goals without copying them; the staticcloneVCExprin the builder, a second deep copy, is gone.substitute,rename,inlineAt,approximate,fold, and the renaming inreadsApartrewrite throughrewriteSlots, which copies a node once, only when a child changes.Sourceis the one field written after a term is shared: a function of itsLocalone (mutable). Annotation visits each node once and writes only a value that differs, so it never writes a node that an earlier module shares and other threads may read.Done(a variable is encoded by its name, and the traversal keeps no scope), the certifier'sCostly, and the Lean printer'sletnames, which now name a large subterm once where a statement shares it rather than once per copy.Measured
llvm_uleb128.cppandsafe_fib.cpp, 4 cores, one job unless stated:main--lower-only: time, peak address space--jobs=8: peak RSS, timesafe_fib.cpp,--lower-only: time, peak address spaceThe 31 verdicts of the case study are the same, line for line.
The exhaustive decoder (
suite/llvm_uleb128_decoder_exhaustive.cpp,UNSUPPORTEDin the suite for its memory) with its own--jobs=8 --timeout=30000: onmainit takes about 87 GB and 1655 s, and its length-10 function times out; here all 78 functions verify in 279 s, peaking at 9.8 GB (16 cores), now the eight jobs' Z3 queries rather than Layer 3.--lower-onlywith one job, which onmainpassed 57 GB in one function before it was stopped, takes 1229 s within 1.5 GB of address space: memory no longer limits it, and the time is the traversals that stay quadratic (below).The Verify suite on a release build: 443 passed, 1 unsupported (the exhaustive decoder, as on
main), in 1036 s with 32 workers.Tests
suite/shared_logic_terms.test(Linux): underulimit -v 2000000(2 GB of address space),--lower-onlylowers every function ofllvm_uleb128.cppandsafe_fib.cpp. Onmainboth runs end withLLVM ERROR: out of memory. It also checks the Layer-3 rewrite count of a function whose goals share rewritten subterms (set_valueofbackend_counterexample_trace.cpp, 265, as onmain), and exports the case study to Lean and checks withInputs/lean_let_names.pythat no statement binds oneletname twice in the lets it starts with.Mutants, each built and run on
shared_logic_terms.test,obligation_ir.cpp,obligation_simplify.cpp,obligation_archive.test,certified_quantifier_ranges.cpp,nested_quantifier_counterexamples.cpp,choose.cpp,choose_axioms.cpp,inductive_counterexamples.cpp,inductive_predicates.cpp,spec_fuel_counterexamples.cpp, andread_instances.cpp(the two in the certifier also on theCertifyTestandMachineIntegerEncodingTestunit tests):shared_logic_terms(out of memory)shared_logic_terms(lets)shared_logic_terms(rewrites 265ofset_valueinbackend_counterexample_trace.cpp, 124 under the mutant; 1515 simplification lines of the suite's dumps change)obligation_archive,nested_quantifier_counterexamples,read_instancessafe_fib.cpp7.9 s and 0.41 GBThe three simplifier mutants change only time and memory, by the factors shown; no output depends on them, and the memo and copy-on-write stay for those factors and for keeping a rewritten shared subterm shared. The last two are in code this change rewrote from in-place to copy-on-write, but they are not new gaps:
main'srenameandsubstitutehave the same two conditions, and no test makes the certifier rename a binder apart inside a quantifier that rebinds the name (an inlined definition whose quantifier's binder is a name the argument mentions).A first version of the annotation mutant (
&& falseon the last comparison of the condition) disabled the write only when the end column alone differed, and survived; the mutant above makes it never write.Equivalence with
mainOver every Verify source (467 files),
main's binary and this branch's were run the same three ways and their outputs compared by SHA-256:--lower-only --dump-ir=3,4 --obligation-out=...: Layer 3 (with every semantic hash) and Layer 4 dumps and the archive are byte-identical for 466 files. The other,crash_report_url.cpp, crashes on purpose and prints the binary's name.--backend=bmc --unroll=2 --lower-only --dump-ir=3: identical for the same 466.--backend=lean --lean-out=...: identical for 458. In the other 8 (the ULEB128 case study and its portfolio input,mvp_factorial,safe_fib,pointer_subobjects, and the three UTF-8 files) a statement names a large subterm it shares once instead of once per copy: inencode_uleb128's obligation 213, 15 lets instead of 16, one of which bound the same 1630-character term twice. Lean checked that each of the 1053 theorems of those 8 exports has the same type onmainand here, up to definitional equality (both exports in one file, each in its own namespace,isDefEqwith every constant of one mapped to the other's).Docs
Checked
VerifyTests: 62 pass, 1 skipped (as onmain);docs_coverage.testanddocs_examples.testpass.shared_logic_terms.testonmain's binary: both lowering runs end withLLVM ERROR: out of memory.