Observed on a rayforce-git build of dev: 2.6.2.r219.g88c03a90.
Summary
Rayfall has no way to stop iterating before the end of a sequence. Every
iteration primitive visits every element, and there is no loop construct, so a
"repeat until done" loop has to be written as a fold over a fixed range whose
full length is paid on every call — however early the work actually finishes.
We would like an early-terminating iteration form: a while special form, a
conditional fold, or the While form of a fold/scan operator. Any one of them
solves it.
Why this cannot be expressed today
Control flow has no loop. Per the function reference, the Control Flow &
Special Forms section is set, let, if, do, fn, try, raise,
return, quote, alter, del. There is no while, do[n;...], or
until.
Every iteration primitive is exhaustive. map, pmap, fold,
fold-left, fold-right, scan, scan-left, scan-right and prior all
consume the whole input. None takes a predicate that can stop them.
return does not help. It exits the lambda, not the iteration. The fold
keeps going and calls the lambda for every remaining element:
(set n 0)
(set probe
(fn []
(fold-left (fn [acc i] (do (set n (+ n 1)) (if (> i 3) (return 'early) (+ acc i)))) 0 (til 10))))
(show (list 'result (probe) 'lambda_calls n))
;; => (result early lambda_calls 10)
Ten invocations, though the work was finished after four.
Recursion is not an alternative. Rayfall has no tail-call elimination and
the stack tops out between roughly 1000 and 2000 frames (measured), so a
recursive loop over a queue of unbounded length overflows. That is what pushed
us to the bounded fold in the first place.
What it costs us
We run a market-data service in Rayfall that drains a queue of completed
messages on every incoming batch. The natural shape is "process one wave,
repeat until nothing is left". Written with the tools available, that is:
(set DRAIN_STEPS 4096)
(set loop_
(fn [step]
(fold-left (fn [more _i] (if more (step) false)) true (til DRAIN_STEPS))))
DRAIN_STEPS is a safety bound for a pathological batch, not a step count we
expect to reach — a normal batch drains in a handful of steps. But the fold
cannot stop, so every call:
- allocates a 4096-element range, and
- evaluates
(if more ... false) 4096 times, almost all of them after the
drain has already finished.
At our ingest rate (~107 batches/s) that is roughly 440,000 interpreted lambda
invocations per second whose only purpose is to re-establish that there is
nothing left to do, plus a discarded 4096-element allocation per batch. It is
visible in a perf profile of the running service.
The workaround, and why it is not a fix
We now nest two folds of 64, so an early finish costs one block plus a pass of
the outer fold (~128 iterations) instead of 4096:
(set DRAIN_BLOCK (til 64))
(set DRAIN_BLOCKS (til (max 1 (div (+ DRAIN_STEPS 63) 64))))
(set loop_
(fn [step]
(if (fold-left
(fn [more _b]
(if more (fold-left (fn [m _i] (if m (step) false)) true DRAIN_BLOCK) false))
true DRAIN_BLOCKS)
(set drain_truncations (+ drain_truncations 1))
null)))
This is a 32x improvement and still the wrong thing. It is not zero, the block
size is an arbitrary constant traded against the bound, and the bound now has
to be rounded up to a multiple of the block size so a tuned value is not
silently lowered. None of that would exist with a loop that stops.
What we are asking for
Any one of these would be enough, in rough order of preference:
- A
while special form — (while cond-fn body-fn), iterative, no stack
growth.
- A conditional fold — e.g.
(fold-while pred f init xs), stopping at the
first element where pred fails.
- A converge / While form on the existing fold family, matching how
/
already works in q.
The first is the most direct fit for "repeat until done", where there is no
sequence to fold over in the first place — the range in our code is pure
scaffolding.
Prior art
q, whose vocabulary Rayfall already mirrors closely (fold, scan, prior,
map), provides all three shapes:
while[cond; body] / statement form
do[n; body] / bounded statement form
f/[x] / Converge: until the result stops changing
n f/[x] / Do: exactly n times
{x<100} {x*2}/[1] / While: until the condition fails -> 128
The While form of Over is exactly the missing construct: iterate until the
step says stop. It terminates on the condition, allocates no range, and does
not recurse.
Given fold-left, scan and prior already exist with q-like semantics, this
would extend the existing vocabulary rather than introduce a new concept.
Secondary note: stack depth
Worth flagging alongside: the absence of tail-call elimination is the root
reason the loop cannot simply be recursive. Either an early-terminating loop
form or TCE would remove the need for a step bound entirely. A loop form is
the smaller and more predictable change, which is why it is the request here.
Observed on a
rayforce-gitbuild ofdev: 2.6.2.r219.g88c03a90.Summary
Rayfall has no way to stop iterating before the end of a sequence. Every
iteration primitive visits every element, and there is no loop construct, so a
"repeat until done" loop has to be written as a fold over a fixed range whose
full length is paid on every call — however early the work actually finishes.
We would like an early-terminating iteration form: a
whilespecial form, aconditional fold, or the While form of a fold/scan operator. Any one of them
solves it.
Why this cannot be expressed today
Control flow has no loop. Per the function reference, the Control Flow &
Special Forms section is
set,let,if,do,fn,try,raise,return,quote,alter,del. There is nowhile,do[n;...], oruntil.Every iteration primitive is exhaustive.
map,pmap,fold,fold-left,fold-right,scan,scan-left,scan-rightandpriorallconsume the whole input. None takes a predicate that can stop them.
returndoes not help. It exits the lambda, not the iteration. The foldkeeps going and calls the lambda for every remaining element:
Ten invocations, though the work was finished after four.
Recursion is not an alternative. Rayfall has no tail-call elimination and
the stack tops out between roughly 1000 and 2000 frames (measured), so a
recursive loop over a queue of unbounded length overflows. That is what pushed
us to the bounded fold in the first place.
What it costs us
We run a market-data service in Rayfall that drains a queue of completed
messages on every incoming batch. The natural shape is "process one wave,
repeat until nothing is left". Written with the tools available, that is:
DRAIN_STEPSis a safety bound for a pathological batch, not a step count weexpect to reach — a normal batch drains in a handful of steps. But the fold
cannot stop, so every call:
(if more ... false)4096 times, almost all of them after thedrain has already finished.
At our ingest rate (~107 batches/s) that is roughly 440,000 interpreted lambda
invocations per second whose only purpose is to re-establish that there is
nothing left to do, plus a discarded 4096-element allocation per batch. It is
visible in a
perfprofile of the running service.The workaround, and why it is not a fix
We now nest two folds of 64, so an early finish costs one block plus a pass of
the outer fold (~128 iterations) instead of 4096:
This is a 32x improvement and still the wrong thing. It is not zero, the block
size is an arbitrary constant traded against the bound, and the bound now has
to be rounded up to a multiple of the block size so a tuned value is not
silently lowered. None of that would exist with a loop that stops.
What we are asking for
Any one of these would be enough, in rough order of preference:
whilespecial form —(while cond-fn body-fn), iterative, no stackgrowth.
(fold-while pred f init xs), stopping at thefirst element where
predfails./already works in q.
The first is the most direct fit for "repeat until done", where there is no
sequence to fold over in the first place — the range in our code is pure
scaffolding.
Prior art
q, whose vocabulary Rayfall already mirrors closely (
fold,scan,prior,map), provides all three shapes:The While form of Over is exactly the missing construct: iterate until the
step says stop. It terminates on the condition, allocates no range, and does
not recurse.
Given
fold-left,scanandprioralready exist with q-like semantics, thiswould extend the existing vocabulary rather than introduce a new concept.
Secondary note: stack depth
Worth flagging alongside: the absence of tail-call elimination is the root
reason the loop cannot simply be recursive. Either an early-terminating loop
form or TCE would remove the need for a step bound entirely. A loop form is
the smaller and more predictable change, which is why it is the request here.