Repository navigation
state_exec: native calls past the first level are the evaluator's - #689
Merged
Merged
Conversation
Inside an evaluator step, the native function the step applies may call a native function itself; a native call asked for from deeper is deferred to step_data, which makes it at its own level (data_step_scope::calls_here). (apply apply L), send through itself and a constructor that is its own init are now loops of steps that max_steps and max_time stop the same way on every platform, instead of hitting the stack-dependent nesting limit. A forwarding call job gives its place to its callee's work, so such chains run in constant space. Inherited init/finalize and the constructor look up the inherited chain 64 classes a call instead of calling through it. pending_data_work chains are freed a link at a time. native_nesting stays the backstop for generators and native calls from C++ with no evaluator.
…ive-calls # Conflicts: # src/cvc/tests/state_exec_deep_data_test.cpp
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.
Follow-up to the deep-data work (#678, #682, #683; #687 fixed a macOS failure in the nesting test this rewrites).
Why
A native function that called a native function (
apply,send, a class constructor runninginit, an inheritedinitrunning its superclass's, a job's call) called it on the C++ stack.detail::native_nestingbounded that with a level count (256) and a stack reserve (128 KiB on Windows, 32 KiB elsewhere). So where a runaway like(apply apply L), withLcontaining itself, stopped depended on how big each platform's frames are; the macOS failure #687 fixed came from that.Native calls past the first level are the evaluator's
applycalling+). A native call asked for from any deeper is deferred to the evaluator, which makes it at its own level in a data step, the way it already ran closures.data_step_scoperemembers the native depth when the step began, andcalls_here()answers "is this the step's own native call, with room left in the slice".call_functionandrun_data_jobboth use it.(apply apply L),sendre-sending through itself, or a class whoseinitis its own constructor is now a sequence of steps.max_stepsandmax_timestop it as they stop(while t nil), at the same step on every platform and stack size.call_walk, newdata_job::forwards_call()) gives its place to the work its callee leaves (step_data). Soapplyapplyingapply, orsendre-sending, runs in constant stack and pending work.init/finalizelook up the chain of classes that inherit them too, 64 classes a call (kInheritedHops), and call the first class's own method. The constructor does the same, and when no class up the chain definesinitit calls nothing. A longer chain, or one that loops, goes on as calls.native_nestingstays as the backstop): the one direct call, a generator stepping its own evaluator (generator_walk), and native calls from C++ with no evaluator step that can take them (call_functionoutside a program,call_here/run_to_completion).A crash fixed on the way
pending_data_work::outerchains were freed recursively, one native frame per link. A closureinitthat constructs again ((defclass P (init (self) (P)))) builds such a chain one link per level on master too, so freeing a killed process could overflow the stack. The chain is now freed a link at a time. With the old destructor,ALongChainOfWaitingWorkIsFreedWithoutRecursioncrashes on a 64 KiB stack.Behavior changes
(apply apply L),sendthrough itself and a constructor that is its owninitno longer throw "native calls nested too deeply". They run untilmax_steps/max_time. Chains that used to hit the limit (10000 nestedapplys, 10000 inheritedinits, 5000 nestedsends) now finish on any stack.forkcalled that way returns -1, as for any call in flight.initpassesinitone argument more per level. It waits a link of pending work per level, and pending work isn't part of the measured heap (max_memory), so onlymax_steps/max_timestop it. The same gap already existed for a closureinitthat constructs again.Cost
New
ExecBenchTest.NativeCallThroughput(instate_exec_bench_test,CVC_STATE_EXEC_BENCH=1). Each case runs a 10000-iteration loop 20 times; the numbers are ns per iteration, the minimum of 6 interleaved rounds (MSVC 14.38 Release, Windows). "Defer every call" is this PR withcalls_here()always false: it shows what the one direct level is for.(list i 1)(no nested call)(apply list (list i 1))(apply apply (list list (list i 1)))(send o "m" i),mnative(K),initinherited fromP(D),initinherited up 4 classesapplyofapply) is about 13% slower.applyandsendcost what they did, within noise.initis 22-28% faster, since the chain is looked up instead of called.EvalFibonacci(3.57 → 3.54 ms),StdlibJobThroughput(map with a native function 45 → 45 ns a node, with a lambda 331 → 310) and the small-calls loop (34.5 → 34.0 ms) are within noise.Tests
StateExecNativeNestinginstate_exec_deep_data_test:ApplyOnASelfContainingList: killed bymax_stepsat exactly 20000 steps, on a stack 64 KiB above the reserve and on 4 MiB.ARunawayCallChainRunsInConstantSpace: 100000 steps of the cycle, with the stack and the pending work the same size throughout.AChainOfAppliesFinishesOnASmallStack:apply10002 levels deep gives 3, sliced (data.step_nodes4) or not.SendThroughItself: 5000 nested sends reach the real method; half the steps it needs kills it at that budget.ConstructorThatIsItsOwnInit: killed bymax_steps.ALongChainOfWaitingWorkIsFreedWithoutRecursion: a 20000-link chain freed on a 64 KiB stack.InheritedInitUpALongChain: 10000 classes construct on a small stack, and a definedinit1000 classes up runs once per construction.AnInheritedInitThatInheritsItself: a looping chain is killed bymax_steps, not hung inside a step.NativeCallsFromCxxWithoutAnEvaluatorNest: the backstop, forcall_functionand for a job run to completion.GeneratorsOfGeneratorsstill expects the nesting limit.Docs: "Native call nesting" in
docs/STATE_EXEC_GRAMMAR.md, and "Function calls" there and in the developer guide.Local results (MSVC 14.38 Release, Windows, cvcpkg prefix)
StateLifetime.ConcurrentPinnedReadVsSweep,StateSessionStatusTest.StatusReportsPumpCyclesandDelegationSecurityTest.DelegationWithLeaseExpiry(a 1 ms lease).ApplyOnASelfContainingListgives way to this one, whereapplyno longer nests.state_exec_deep_data_testpasses at 64 KiB (CVC_DEEP_DATA_STACK_KIB=64).