performance/deep_call_in_fork.bend
bend-lint-rules@0.1.0.0 · 244 lines · 0 definitions
1# performance/deep_call_in_fork: a fork that calls a definition that calls itself outside a tail
2# position. A value of a parallel let (`a b = f(x) g(y)`), or of a let just before it, calls a definition
3# of the file whose call of itself is not the last thing it does.
4# Heuristics: Bend gives a call of itself that is not a tail call a frame per call ("bend guide
5# shaders"). In bend-lint's benchmark (Bend 2.0.36, on the CPU), such a call at each fork cut the speedup
6# from 10x to 1.9x; the same definition with a tail call kept the 10x. In a raytracer that splits its
7# rows that way at each of 9 fork levels, tail-recursive splits made it 4% to 8% faster on the CPU; in
8# Bend's slash boss demo, the same change changed no timing, as its list was short. The JS lane is not
9# measured. A tail position is the body of a definition, a case, or the rest of a block after its lets,
10# through annotations. A definition with a parallel let is a fork tree: its calls of itself are the
11# values of its forks, not frames, and it is not reported (Bend's matrix and Merkle benchmarks call such
12# trees in their forks).
13import Base
14import bend-lint-api@0.1.0.0/linter/lint.bend as Lint
15import bend-lint-api@0.1.0.0/shared/lists.bend as Lists
16import bend-lint-rules@0.1.0.0/performance/gpu_fit.bend as GpuFit
17
18# Types
19# =====
20
21# What judging the calls in one definition reads: the definitions of the file that call themselves outside a tail
22# position (as the check names them), and the definition, by the name the check gives it and as written.
23type Owner is Data:
24 Owner{deeps: List<&2, String>, target: String, name: String}
25
26# Functions
27# =========
28
29# Whether `at` is the place of the last of `nodes`.
30def is_last(nodes: List<&2, Lint.Node>, at: U32) -> Bool:
31 U32.is_eq((at + 1: U32), U32.from_nat(Lists.length(&2, Lint.Node, nodes)))
32
33# Whether the node below `path` is in a tail position, if it ends the node above it (`ends`): the body of a definition
34# or a case, or the rest of a block after its lets, through annotations.
35def is_tail(path: List<&2, Lint.Above>, ends: Bool) -> Bool:
36 match path ends:
37 case _ False{}:
38 False{}
39 case Con{Lint.Above{Lint.Node{Lint.Body{}, n, s, k, f}, at}, rest} True{}:
40 True{}
41 case Con{Lint.Above{Lint.Node{Lint.Case{}, n, s, k, f}, at}, rest} True{}:
42 is_tail(rest, True{})
43 case Con{Lint.Above{Lint.Node{Lint.Match{}, n, s, k, f}, at}, rest} True{}:
44 is_tail(rest, True{})
45 case Con{Lint.Above{Lint.Node{Lint.Annotation{}, n, s, k, f}, at}, rest} True{}:
46 is_tail(rest, True{})
47 case Con{Lint.Above{Lint.Node{Lint.Destructure{q}, n, s, kids, f}, at}, rest} True{}:
48 is_tail(rest, is_last(kids, at))
49 case Con{Lint.Above{Lint.Node{Lint.Let{}, n, s, kids, f}, at}, rest} True{}:
50 is_tail(rest, is_last(kids, at))
51 case _ _:
52 False{}
53
54# Whether a call of its definition to itself below `path` is outside a tail position.
55def deep.of(recursive: Bool, path: List<&2, Lint.Above>) -> List<&2, Bool>:
56 match recursive:
57 case True{}:
58 [Bool.not(is_tail(path, True{}))]
59 case False{}:
60 []
61
62def deep.visit(+u: Unit, +path: List<&2, Lint.Above>, +node: Lint.Node) -> List<&2, Bool>:
63 deep.of(Lint.is_recursive(path, node), path)
64
65# The definitions among `nodes` that call themselves outside a tail position, as the check names them, onto `out`;
66# with `forking` false, not those with a parallel let, whose such calls are the values of their forks.
67def deep(nodes: List<&2, Lint.Node>, +forking: Bool, out: List<&2, String>) -> List<&2, String>:
68 match nodes:
69 case Nil{}:
70 out
71 case Con{Lint.Node{Lint.Definition{+t, u}, n, s, +k, f}, rest}:
72 deep(
73 rest,
74 forking,
75 Lists.push(
76 String,
77 Lists.when(
78 String,
79 List.contains(
80 ~Bool,
81 ~Bool.and,
82 Lint.visit(
83 ~Unit,
84 ~Bool,
85 ~deep.visit,
86 Unit{},
87 [Lint.Node{Lint.Definition{t, u}, n, s, k, f}]
88 ),
89 True{}
90 )
91 && (forking || Bool.not(GpuFit.forks(k, False{}))),
92 t
93 ),
94 out
95 )
96 )
97 case Con{node, rest}:
98 deep(rest, forking, out)
99
100def is_pattern(syntax: Lint.Syntax) -> Bool:
101 match syntax:
102 case Lint.PatternVariable{b, q}:
103 True{}
104 case Lint.PatternConstructor{t}:
105 True{}
106 case _:
107 False{}
108
109def binders(nodes: List<&2, Lint.Node>, n: Nat) -> Nat:
110 match nodes:
111 case Nil{}:
112 n
113 case Con{Lint.Node{Lint.PatternVariable{b, q}, nm, s, k, f}, rest}:
114 binders(rest, 1n+n)
115 case Con{node, rest}:
116 binders(rest, n)
117
118# Whether a let leads to a fork: it is one (it binds `many` names), or it binds one (`one`), and the last of `nodes`,
119# the rest of its block, is a let that leads to one. The values of these lets are what a fork runs, or what runs just
120# before it.
121def leads(nodes: List<&2, Lint.Node>, many: Bool, one: Bool) -> Bool:
122 match nodes many one:
123 case _ True{} _:
124 True{}
125 case _ False{} False{}:
126 False{}
127 case Con{Lint.Node{Lint.Destructure{q}, n, s, +kids, f}, Nil{}} False{} True{}:
128 +count = binders(kids, 0n)
129 leads(kids, Nat.is_gt(count, 1n), Nat.is_eq(count, 1n))
130 case Con{x, Nil{}} False{} True{}:
131 False{}
132 case Con{x, rest} False{} True{}:
133 leads(rest, False{}, True{})
134 case Nil{} False{} True{}:
135 False{}
136
137# Whether `x`, the node at `at` under `parent`, is a value of a fork, or of a let just before one: not a binder, nor the
138# rest of the block.
139def is_value(x: Lint.Node, +parent: Lint.Node, at: U32) -> Bool:
140 match x parent:
141 case Lint.Node{s, n, sp, k, f} Lint.Node{ps, pn, psp, +kids, pf}:
142 leads([parent], False{}, Bool.not(is_pattern(s)) && Bool.not(is_last(kids, at)))
143
144# Whether `x`, below `path`, or a node above it, is a value of a fork, or `found`.
145def is_inside(path: List<&2, Lint.Above>, x: Lint.Node, found: Bool) -> Bool:
146 match path found:
147 case _ True{}:
148 True{}
149 case Nil{} False{}:
150 False{}
151 case Con{Lint.Above{+p, at}, rest} False{}:
152 is_inside(rest, p, is_value(x, p, at))
153
154def message(+callee: String, loop: String) -> String:
155 callee
156 ++ " calls itself outside a tail position, so Bend gives each of its calls a frame, and "
157 ++ loop
158 ++ " calls it in a fork (a value of its parallel let, or of a let just before it). CPU: measured 4% to 8% faster for a raytracer after the fix; in a benchmark, such a call at each fork cut a 10x speedup to 1.9x. GPU: such a recursion in a `!` made a draw 2.3x slower (\"bend guide shaders\"). JS: not measured. Confidence: medium, as the cost grows with the depth of the recursion. Make "
159 ++ callee
160 ++ " tail-recursive with an accumulator, compute its value with native operations (such as U32.shln), or compute it once and pass it down as a parameter. If its input is short, expect this finding."
161
162# The finding about a call of a deep definition (if `deep`), when the call is in a fork.
163def judge.deep(
164 deep: Bool,
165 path: List<&2, Lint.Above>,
166 +node: Lint.Node,
167 callee: String,
168 name: String
169) -> List<&2, Lint.Finding>:
170 match deep:
171 case True{}:
172 Lint.finding(
173 Lint.Information{},
174 is_inside(path, node, False{}),
175 Lint.Node.span(Some{node}),
176 message(callee, name)
177 )
178 case False{}:
179 []
180
181def judge.call(
182 +owner: Owner,
183 path: List<&2, Lint.Above>,
184 node: Lint.Node,
185 callee: String,
186 +called: String
187) -> List<&2, Lint.Finding>:
188 match owner:
189 case Owner{deeps, target, name}:
190 judge.deep(
191 List.contains(~String, ~String.eq, deeps, called) && Bool.not(String.eq(called, target)),
192 path,
193 node,
194 callee,
195 name
196 )
197
198def visit(+owner: Owner, +path: List<&2, Lint.Above>, +node: Lint.Node) -> List<&2, Lint.Finding>:
199 match node:
200 case Lint.Node{
201 Lint.Application{},
202 n,
203 s,
204 Con{Lint.Node{Lint.Reference{called}, callee, rs, rk, rf}, args},
205 f
206 }:
207 judge.call(owner, path, node, callee, called)
208 case _:
209 []
210
211def defs(
212 nodes: List<&2, Lint.Node>,
213 +deeps: List<&2, String>,
214 out: List<&2, Lint.Finding>
215) -> List<&2, Lint.Finding>:
216 match nodes:
217 case Nil{}:
218 List.reverse(&2, Lint.Finding, out)
219 case Con{Lint.Node{Lint.Definition{+t, u}, +n, s, k, f}, rest}:
220 defs(
221 rest,
222 deeps,
223 Lists.onto(
224 &2,
225 Lint.Finding,
226 Lint.visit(
227 ~Owner,
228 ~Lint.Finding,
229 ~visit,
230 Owner{deeps, t, n},
231 [Lint.Node{Lint.Definition{t, u}, n, s, k, f}]
232 ),
233 out
234 )
235 )
236 case Con{node, rest}:
237 defs(rest, deeps, out)
238
239def run(+here: Lint.Here) -> Lint.Ask(List<&2, Lint.Finding>):
240 +nodes = Lint.Here.nodes(here)
241 Lint.done(defs(nodes, deep(nodes, False{}, []), []))
242
243def rule() -> Lint.Rule:
244 Lint.Rule{Lint.Needs{False{}, False{}}, [], Lint.OnFile{run}}