Conversation
c7aa8e1 to
e982a19
Compare
Add a .parallel() macro that mirrors .map() but evaluates body
expressions concurrently. Three call forms are supported:
range.parallel(v, expr) // apply to every element
range.parallel(v, pred, expr) // apply where pred is true
range.parallel(v, v2, expr) // two-variable (index/key + value)
Concurrency is controlled by two knobs set at the environment level
via Parallel(perCall, globalCap):
- perCall bounds goroutines per .parallel() invocation (defaults to
GOMAXPROCS).
- globalCap bounds total concurrent leaf evaluations across all
.parallel() calls in the program (0 means unlimited).
When perCall == 1 the macro degenerates to a sequential comprehension
with no goroutine overhead.
Output order always matches input order. Errors are surfaced
deterministically: the first error in iteration order is returned.
The implementation uses sentinel function calls (@parallel,
@parallel_body, @parallel_pred) in the AST so that decorators can
recover the planned body and predicate Interpretables at program
construction time, working around the unexported evalFold in cel-go.
| // # Call forms | ||
| // | ||
| // parallel mirrors the call forms of the built-in map macro: | ||
| // | ||
| // // (1) One-variable — apply expr to every element. | ||
| // <list>.parallel(<iterVar>, <expr>) -> list<dyn> | ||
| // <map>.parallel(<iterVar>, <expr>) -> list<dyn> | ||
| // | ||
| // // (2a) Predicate filter — apply expr only where pred is true. | ||
| // <list>.parallel(<iterVar>, <pred>, <expr>) -> list<dyn> | ||
| // <map>.parallel(<iterVar>, <pred>, <expr>) -> list<dyn> | ||
| // | ||
| // // (2b) Two-variable — bind index/key and value simultaneously. | ||
| // <list>.parallel(<iterVar>, <iterVar2>, <expr>) -> list<dyn> | ||
| // <map>.parallel(<iterVar>, <iterVar2>, <expr>) -> list<dyn> | ||
| // | ||
| // Forms (2a) and (2b) are distinguished by their second argument: if it is a | ||
| // plain identifier it is treated as iterVar2 (two-variable form); otherwise it | ||
| // is treated as a boolean predicate (filter form). This is the same rule used | ||
| // by map. | ||
| // | ||
| // For list ranges in the two-variable form, iterVar is the zero-based element | ||
| // index and iterVar2 is the element value. For map ranges, iterVar is the key | ||
| // and iterVar2 is the corresponding value. |
There was a problem hiding this comment.
Would be nice to have map in the name, like parallel_map or pmap, which are used in some other languages/libraries.
There was a problem hiding this comment.
It would be good to have a comment that says a bit more explicitly how it differs from map.
From what I can see in the comments and explanatory notes, it's going to do the same as map, except that it runs in parallel, and if one item fails it won't cancel the others. So failures may run longer (due to processing all items rather then stopping at the first failure), but successes will likely run quicker (due to parallelism). If there are multiple failures you'll get the first one from the list/map order, not time order, matching map. Is that right? Are there any other differences.
There was a problem hiding this comment.
Can anything go wrong in usage, besides using a custom function that isn't safe for concurrency?
There was a problem hiding this comment.
I guess the global_concurrency setting would stop someone starting too many goroutines by nesting parallel calls. That's good.
There was a problem hiding this comment.
pmap was the original name. I moved to parallel to increase friction. I think parallel_map is too far and does not significantly improve comprehensibility (if this tips a user over the edge into thinking that they understand the consequences of using it, they probably should not be using it).
It would be good to have a comment that says a bit more explicitly how it differs from map.
Semantically (ignoring parallelism), it is identical to map. In terms of wall time, yes it may run longer in the case that it fails on one and the failure should cancel the full set of evaluations. This is not semantics though.
Can anything go wrong in usage, besides using a custom function that isn't safe for concurrency?
Quicker system resource exhaustion? Otherwise no.
There was a problem hiding this comment.
Have you thought about getting it into cel-go?
There was a problem hiding this comment.
No. The use there is almost entirely absent. CEL in the conventional usage is for dynamic configuration logic, and should almost never have workloads that would be helped by this. Adding it would be an unnecessary maintenance burden on that project.
There is merit in filing an issue (marked in a TODO here) to make the work we do easier. That should be accepted, but still needs discussion with cel. I'd like to have this in place and working as an evidence point for that request before I file it.
See commit message for details. A detailed explanation of the mechanism is available on request.