performance/repeated_call.bend
bend-lint-rules@0.2.1.0 · 299 lines · 0 definitions
1# performance/repeated_call: a call made again, with the same arguments, on one path of a definition: a
2# second call of a definition of the file that loops and returns a number, with its arguments written the
3# same way, in the same branch (the innermost case around both, or the definition's body).
4# Heuristics: clang does not merge two calls of a definition that loops: Bend runs each through its own
5# calls. Measured on the CPU, pow2(30) written `Nat.add(pow2(q), pow2(q))` took 3.4 s; with the two calls
6# forked (a parallel let), 0.6 s; computed once (`+h = pow2(q)`), 0.1 s, the program's start. In
7# eldergrove-faire and in llama.bend's tokenizer, computing such calls once changed nothing measurable:
8# they walk short lists. Arguments are the same when written the same way: the same variables, literals,
9# and calls or constructions of them. Calls in a lambda, in a type, in a proof, or in the values of
10# Bool.pick, `&&` or `||` (which performance/eager_branch judges) do not count, nor do the values of one
11# parallel let: copies made in parallel on purpose, as benchmarks do. A call that
12# performance/loop_invariant_call reports is left to it: computing it once before the loop also ends the
13# repeat. Only a number is sure to be shareable at no cost: data may be affine (an Array), so that it
14# must be built twice, and sharing it counts references; it is skipped. Numbers are those of
15# performance/gpu_fit, as the callee's return type is written.
16import Base
17import bend-lint-api@0.2.1.0/linter/lint.bend as Lint
18import bend-lint-api@0.2.1.0/shared/lists.bend as Lists
19import bend-lint-api@0.2.1.0/shared/names.bend as Names
20import bend-lint-rules@0.2.1.0/performance/deep_call_in_fork.bend as Deep
21import bend-lint-rules@0.2.1.0/performance/gpu_fit.bend as GpuFit
22import bend-lint-rules@0.2.1.0/performance/loop_invariant_call.bend as Invariant
23import bend-lint-rules@0.2.1.0/performance/missed_fork.bend as Fork
24
25# Types
26# =====
27
28# A call of a definition that loops: the definition (as the check names it, and as written), its arguments, where it
29# is written, and where its branch starts (0 for a definition's body).
30type Call is Data:
31 Call{target: String, callee: String, args: List<&2, Lint.Node>, span: Lint.Span, branch: U32}
32
33# What finding the calls of one definition reads: what performance/loop_invariant_call reads to judge them, with the
34# definitions of the file that call themselves (as the check names them), and the program's definitions, by the name
35# the check gives them.
36type Scope is Data:
37 Scope{owner: Invariant.Owner, defs: Map<&2, Maybe<&2, Lint.Def>>}
38
39# Functions
40# =========
41def Call.target(c: Call) -> String:
42 match c:
43 case Call{t, callee, args, span, b}:
44 t
45
46def Call.callee(c: Call) -> String:
47 match c:
48 case Call{t, callee, args, span, b}:
49 callee
50
51def Call.args(c: Call) -> List<&2, Lint.Node>:
52 match c:
53 case Call{t, callee, args, span, b}:
54 args
55
56def Call.span(c: Call) -> Lint.Span:
57 match c:
58 case Call{t, callee, args, span, b}:
59 span
60
61def Call.branch(c: Call) -> U32:
62 match c:
63 case Call{t, callee, args, span, b}:
64 b
65
66def message(callee: String, loop: String) -> String:
67 callee
68 ++ " is called again here, with the same arguments, on this path of "
69 ++ loop
70 ++ ", so Bend computes it twice: clang does not merge calls of a definition that loops. All lanes. Measured on the CPU: pow2(30) written `Nat.add(pow2(q), pow2(q))` took 3.4 s, against 0.1 s after the fix; in eldergrove-faire and in llama.bend's tokenizer, the fix changed nothing measurable, as those calls walk short lists. Confidence: medium, as the gain is what the call costs. Bind its value once, with `+` if it is used twice, and use that name. If the call is cheap, such as a walk of a short list, expect this finding."
71
72# Whether a node below `path` may not run on the path of its branch, or runs as a copy made on purpose: under a lambda,
73# a value of a choice (Bool.pick, Bool.and, Bool.or), or a value of a parallel let.
74def is_guarded(path: List<&2, Lint.Above>, found: Bool) -> Bool:
75 match path found:
76 case _ True{}:
77 True{}
78 case Nil{} False{}:
79 False{}
80 case Con{Lint.Above{Lint.Node{Lint.Lambda{b, q}, n, s, k, f}, at}, rest} False{}:
81 True{}
82 case Con{Lint.Above{Lint.Node{Lint.Application{}, n, s, +kids, f}, at}, rest} False{}:
83 is_guarded(rest, Fork.is_choice(kids))
84 case Con{Lint.Above{Lint.Node{Lint.Destructure{q}, n, s, +kids, f}, at}, rest} False{}:
85 is_guarded(rest, Nat.is_gt(Deep.binders(kids, 0n), 1n) && Bool.not(Deep.is_last(kids, at)))
86 case Con{a, rest} False{}:
87 is_guarded(rest, False{})
88
89# Where the first of `nodes`, or a node under it, that has a place starts.
90def first_beg(+fuel: Nat, nodes: List<&2, Lint.Node>) -> U32:
91 match fuel nodes:
92 case 0n _:
93 0
94 case _ Nil{}:
95 0
96 case 1n+f Con{Lint.Node{syntax, n, Some{Lint.Span{p, beg, end}}, k, fs}, rest}:
97 beg
98 case 1n+f Con{Lint.Node{syntax, n, None{}, k, fs}, rest}:
99 first_beg(f, List.append(&2, Lint.Node, k, rest))
100
101# Where a branch starts (a case has no place of its own: its first node with one), or 0 for a definition's body.
102def branch.at(b: Maybe<&2, Lint.Node>) -> U32:
103 match b:
104 case Some{node}:
105 first_beg(64n, [node])
106 case None{}:
107 0
108
109# Whether a definition of the program says it returns a number.
110def returns_number(info: Maybe<&2, Lint.Def>) -> Bool:
111 match info:
112 case Some{Lint.Def{t, ps, fk, cs, r}}:
113 GpuFit.is_scalar(r)
114 case None{}:
115 False{}
116
117# Whether a call that counts so far (if `ok`) still counts: not if performance/loop_invariant_call reports it.
118def is_kept(ok: Bool, owner: Invariant.Owner, path: List<&2, Lint.Above>, node: Lint.Node) -> Bool:
119 match ok:
120 case True{}:
121 List.is_empty(&2, Lint.Finding, Invariant.visit(owner, path, node))
122 case False{}:
123 False{}
124
125# The call of a definition that loops at an application below `path`, if it counts.
126def visit(+scope: Scope, +path: List<&2, Lint.Above>, +node: Lint.Node) -> List<&2, Call>:
127 match scope node:
128 case Scope{
129 Invariant.Owner{+loops, target, name, fixed},
130 defs
131 } Lint.Node{
132 Lint.Application{},
133 n,
134 Some{span},
135 Con{Lint.Node{Lint.Reference{+t}, callee, rs, rk, rf}, args},
136 f
137 }:
138 Bool.pick(
139 List<&2, Call>,
140 is_kept(
141 List.contains(
142 ~String,
143 ~String.eq,
144 loops,
145 t
146 )
147 && returns_number(Names.find(Lint.Def, defs, t))
148 && Lint.is_live(path)
149 && Bool.not(is_guarded(path, False{})),
150 Invariant.Owner{loops, target, name, fixed},
151 path,
152 node
153 ),
154 [Call{t, callee, args, span, branch.at(Invariant.branch(path, None{}))}],
155 []
156 )
157 case _ _:
158 []
159
160# Whether two nodes, as written, are the same at their top: the same variable, literal or reference, or the same kind
161# of application, annotation or construction (their nodes below are compared next).
162def is_shallow(x: Lint.Node, y: Lint.Node) -> Bool:
163 match x y:
164 case Lint.Node{Lint.Variable{a}, n, s, k, f} Lint.Node{Lint.Variable{b}, m, r, j, g}:
165 U32.is_eq(a, b)
166 case Lint.Node{Lint.Literal{a}, n, s, k, f} Lint.Node{Lint.Literal{b}, m, r, j, g}:
167 String.eq(a, b) && String.eq(n, m)
168 case Lint.Node{Lint.Reference{a}, n, s, k, f} Lint.Node{Lint.Reference{b}, m, r, j, g}:
169 String.eq(a, b)
170 case Lint.Node{Lint.Application{}, n, s, k, f} Lint.Node{Lint.Application{}, m, r, j, g}:
171 True{}
172 case Lint.Node{Lint.Annotation{}, n, s, k, f} Lint.Node{Lint.Annotation{}, m, r, j, g}:
173 True{}
174 case Lint.Node{Lint.Construction{a}, n, s, k, f} Lint.Node{Lint.Construction{b}, m, r, j, g}:
175 String.eq(a, b)
176 case _ _:
177 False{}
178
179# Whether `xs` and `ys` are written the same way, node by node and below (if `ok`); `fuel` bounds the nodes compared.
180def is_same(+fuel: Nat, xs: List<&2, Lint.Node>, ys: List<&2, Lint.Node>, ok: Bool) -> Bool:
181 match fuel xs ys ok:
182 case _ _ _ False{}:
183 False{}
184 case _ Nil{} Nil{} True{}:
185 True{}
186 case 0n _ _ True{}:
187 False{}
188 case 1n+f Con{+x, xt} Con{+y, yt} True{}:
189 is_same(
190 f,
191 List.append(&2, Lint.Node, Lint.Node.kids(x), xt),
192 List.append(&2, Lint.Node, Lint.Node.kids(y), yt),
193 is_shallow(x, y)
194 )
195 case _ _ _ True{}:
196 False{}
197
198# Whether `c` repeats a call of `seen`: the same definition, in the same branch, with arguments written the same way.
199def repeats(seen: List<&2, Call>, +c: Call, found: Bool) -> Bool:
200 match seen found:
201 case _ True{}:
202 True{}
203 case Nil{} False{}:
204 False{}
205 case Con{Call{t, callee, args, span, b}, rest} False{}:
206 repeats(
207 rest,
208 c,
209 is_same(
210 256n,
211 args,
212 Call.args(c),
213 String.eq(t, Call.target(c)) && U32.is_eq(b, Call.branch(c))
214 )
215 )
216
217# The findings about the calls of one definition (`loop`, as written) that repeat an earlier one, onto `out`.
218def judged(
219 calls: List<&2, Call>,
220 +seen: List<&2, Call>,
221 +loop: String,
222 out: List<&2, Lint.Finding>
223) -> List<&2, Lint.Finding>:
224 match calls:
225 case Nil{}:
226 out
227 case Con{+c, rest}:
228 judged(
229 rest,
230 c <> seen,
231 loop,
232 Lists.onto(
233 &2,
234 Lint.Finding,
235 Lint.finding(
236 Lint.Information{},
237 repeats(seen, c, False{}),
238 Some{Call.span(c)},
239 message(Call.callee(c), loop)
240 ),
241 out
242 )
243 )
244
245# The calls of a definition (`node`) that count, unless it is a proof.
246def calls.of(
247 proof: Bool,
248 loops: List<&2, String>,
249 defs: Map<&2, Maybe<&2, Lint.Def>>,
250 +node: Lint.Node
251) -> List<&2, Call>:
252 match proof:
253 case True{}:
254 []
255 case False{}:
256 Lint.visit(~Scope, ~Call, ~visit, Scope{Invariant.Owner.of(loops, node), defs}, [node])
257
258def defs(
259 nodes: List<&2, Lint.Node>,
260 +loops: List<&2, String>,
261 +program: Map<&2, Maybe<&2, Lint.Def>>,
262 out: List<&2, Lint.Finding>
263) -> List<&2, Lint.Finding>:
264 match nodes:
265 case Nil{}:
266 List.reverse(&2, Lint.Finding, out)
267 case Con{Lint.Node{Lint.Definition{t, u}, +n, s, +k, f}, rest}:
268 defs(
269 rest,
270 loops,
271 program,
272 judged(
273 calls.of(
274 Invariant.is_proof(k),
275 loops,
276 program,
277 Lint.Node{Lint.Definition{t, u}, n, s, k, f}
278 ),
279 [],
280 n,
281 out
282 )
283 )
284 case Con{node, rest}:
285 defs(rest, loops, program, out)
286
287def run(+here: Lint.Here) -> Lint.Ask(List<&2, Lint.Finding>):
288 +nodes = Lint.Here.nodes(here)
289 Lint.done(
290 defs(
291 nodes,
292 Invariant.loops(nodes, []),
293 Lint.Program.defs(Lint.Here.sources(here), Map.new(&2, Maybe<&2, Lint.Def>)),
294 []
295 )
296 )
297
298def rule() -> Lint.Rule:
299 Lint.Rule{Lint.Needs{False{}, False{}}, [], Lint.OnFile{run}}