performance/gpu_fit.bend
bend-lint-rules@0.2.0.0 · 352 lines · 26 definitions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import Base
22import bend-lint-api@0.2.0.0/linter/lint.bend as Lint
23import bend-lint-api@0.2.0.0/shared/lists.bend as Lists
24import bend-lint-api@0.2.0.0/shared/names.bend as Names
25import bend-lint-api@0.2.0.0/shared/sets.bend as Sets
26
27
28
29
30
31type Slot is Data:
32 Slot{owner: String, ty: String}
33
34
35
36type Program is Data:
37 Program{
38 defs: Map<&2, Maybe<&2, Lint.Def>>,
39 under: Set(),
40 slots: List<&2, Slot>,
41 bangs: List<&2, U32>
42 }
43
44
45
46
47
48def slots(nodes: List<&2, Lint.Node>, +current: String, out: List<&2, Slot>) -> List<&2, Slot>:
49 match nodes:
50 case Nil{}:
51 out
52 case Con{Lint.Node{Lint.Datatype{t}, n, s, kids, f}, rest}:
53 slots(rest, current, slots(kids, t, out))
54 case Con{Lint.Node{Lint.Constructor{t}, n, s, kids, f}, rest}:
55 slots(rest, current, slots(kids, current, out))
56 case Con{Lint.Node{Lint.Field{b, q}, n, s, kids, f}, rest}:
57 slots(rest, current, Slot{current, Lint.type_name(kids)} <> out)
58 case Con{node, rest}:
59 slots(rest, current, out)
60
61
62
63def is_drop(info: Maybe<&2, Lint.Def>, t: String) -> Bool:
64 match info:
65 case Some{Lint.Def{d, ps, fs, cs, r}}:
66 String.eq(r, "Unit")
67 case None{}:
68 List.contains(~String, ~String.eq, ["Image.drop", "Image.free", "Image.sink"], t)
69
70def program.slots(sources: List<&2, Lint.Source>, out: List<&2, Slot>) -> List<&2, Slot>:
71 match sources:
72 case Nil{}:
73 out
74 case Con{Lint.Source{path, ns, text, nodes}, rest}:
75 program.slots(rest, slots(nodes, "", out))
76
77def is_scalar(t: String) -> Bool:
78 List.contains(
79 ~String,
80 ~String.eq,
81 ["U32", "I32", "F32", "Nat", "Bool", "Char", "U64", "I64", "F64", "Unit"],
82 t
83 )
84
85def fields(slots: List<&2, Slot>, +t: String, out: List<&2, String>) -> List<&2, String>:
86 match slots:
87 case Nil{}:
88 out
89 case Con{Slot{o, ty}, rest}:
90 fields(rest, t, Lists.push(String, Lists.when(String, String.eq(o, t), ty), out))
91
92def is_known(slots: List<&2, Slot>, hit: Bool, +t: String) -> Bool:
93 match slots hit:
94 case _ True{}:
95 True{}
96 case Nil{} False{}:
97 False{}
98 case Con{Slot{o, ty}, rest} False{}:
99 is_known(rest, String.eq(o, t), t)
100
101def is_flat.expand(
102 known: Bool,
103 t: String,
104 rest: List<&2, String>,
105 slots: List<&2, Slot>
106) -> Maybe<&2, List<&2, String>>:
107 match known:
108 case True{}:
109 Some{fields(slots, t, rest)}
110 case False{}:
111 None{}
112
113
114
115def is_flat.next(
116 number: Bool,
117 +t: String,
118 rest: List<&2, String>,
119 +slots: List<&2, Slot>
120) -> Maybe<&2, List<&2, String>>:
121 match number:
122 case True{}:
123 Some{rest}
124 case False{}:
125 is_flat.expand(is_known(slots, False{}, t), t, rest, slots)
126
127
128
129def is_flat(fuel: Nat, todo: Maybe<&2, List<&2, String>>, +slots: List<&2, Slot>) -> Bool:
130 match fuel todo:
131 case _ None{}:
132 False{}
133 case _ Some{Nil{}}:
134 True{}
135 case 0n _:
136 False{}
137 case 1n+f Some{Con{+t, rest}}:
138 is_flat(f, is_flat.next(is_scalar(t), t, rest, slots), slots)
139
140
141def data(
142 params: List<&2, Lint.Param>,
143 found: Maybe<&2, Lint.Param>,
144 +slots: List<&2, Slot>
145) -> Maybe<&2, Lint.Param>:
146 match params found:
147 case _ Some{param}:
148 Some{param}
149 case Nil{} None{}:
150 None{}
151 case Con{Lint.Param{n, +ty}, rest} None{}:
152 data(
153 rest,
154 Lists.when(Lint.Param, Bool.not(is_flat(64n, Some{[ty]}, slots)), Lint.Param{n, ty}),
155 slots
156 )
157
158
159def forking(
160 names: List<&2, String>,
161 +defs: Map<&2, Maybe<&2, Lint.Def>>,
162 out: List<&2, String>
163) -> List<&2, String>:
164 match names:
165 case Nil{}:
166 out
167 case Con{+t, rest}:
168 forking(
169 rest,
170 defs,
171 Lists.push(
172 String,
173 Lists.when(String, Lint.Def.forks(Names.find(Lint.Def, defs, t)), t),
174 out
175 )
176 )
177
178
179
180def program.under(sources: List<&2, Lint.Source>, +defs: Map<&2, Maybe<&2, Lint.Def>>) -> Set():
181 Lint.Program.callers(
182 sources,
183 defs,
184 Set.from_list(
185 forking(Map.keys(&2, Maybe<&2, Lint.Def>, defs), defs, ["Image.free", "Image.drop"])
186 )
187 )
188
189
190def bangs(s: String, +at: U32, out: List<&2, U32>) -> List<&2, U32>:
191 match s:
192 case SNil{}:
193 out
194 case SCon{'!', SCon{'(', rest}}:
195 bangs(rest, (at + 2: U32), at <> out)
196 case SCon{c, rest}:
197 bangs(rest, (at + 1: U32), out)
198
199def alone(+callee: String) -> String:
200 callee
201 ++ "! runs on one GPU thread, alone: no parallel let runs under "
202 ++ callee
203 ++ ". GPU: measured 6.5 s for a list walk of BendSR, against 0.08 s without the `!`. CPU (`--gpu off`): 1.6x slower with the `!`. JS: no GPU, so no change. Confidence: high, as one GPU thread is much slower than a CPU core. Remove the `!`, or fork inside "
204 ++ callee
205 ++ " with a parallel let. If a timing shows the `!` faster here, expect this finding."
206
207def slower(callee: String, param: Lint.Param) -> String:
208 match param:
209 case Lint.Param{name, +ty}:
210 callee
211 ++ "! receives data ("
212 ++ name
213 ++ Bool.pick(String, String.is_empty(ty), "", ": " ++ ty)
214 ++ "), which a discrete GPU reads across PCIe, page by page. GPU: on discrete GPUs (CUDA), measured slower than `--gpu off` for Bend's slash boss draw (1.4x) and for portal-bend; with unified memory (Apple M-series), the GPU wins that draw. CPU (`--gpu off`): not measured. JS: no GPU, so no change. Confidence: medium, as it depends on the machine. Time the program with and without `--gpu off`. If the GPU wins, expect this finding."
215
216def faster(+callee: String) -> String:
217 callee
218 ++ " forks over numbers only, so `"
219 ++ callee
220 ++ "!(..)` can run faster on the GPU, if the call is long: a discrete GPU costs a fixed 0.3 to 0.8 s to start, then numeric work is nearly free. GPU: measured 3.8x faster for a fork tree that took 3.6 s on the CPU, and 8x slower for one that took 0.06 s. CPU (`--gpu off`): not measured. JS: no GPU, so no change. Confidence: low, as it depends on how long this call runs, and on the machine. Add the `!` if this call takes over a second on the CPU, and time both; a fork before it in the same evaluation keeps it on the CPU (performance/bang_after_fork). If the call is short, or already runs under a `!` of a caller (a `!` in a `!` measured up to 4.6x slower), expect this finding."
221
222def Def.target(info: Maybe<&2, Lint.Def>) -> String:
223 match info:
224 case Some{Lint.Def{t, params, forks, calls, r}}:
225 t
226 case None{}:
227 ""
228
229
230
231def unbanged(
232 info: Maybe<&2, Lint.Def>,
233 callee: String,
234 +caller: Maybe<&2, Lint.Def>,
235 slots: List<&2, Slot>,
236 span: Maybe<&2, Lint.Span>
237) -> List<&2, Lint.Finding>:
238 match info:
239 case Some{Lint.Def{t, params, True{}, calls, r}}:
240 Lint.finding(
241 Lint.Hint{},
242 Bool.not(
243 Maybe.is_some(&2, Lint.Param, data(params, None{}, slots))
244 )
245 && Bool.not(Lint.Def.forks(caller))
246 && Bool.not(String.eq(t, Def.target(caller))),
247 span,
248 faster(callee)
249 )
250 case _:
251 []
252
253
254
255def banged.of(
256 forks: Bool,
257 param: Maybe<&2, Lint.Param>,
258 callee: String,
259 span: Maybe<&2, Lint.Span>
260) -> List<&2, Lint.Finding>:
261 match forks param:
262 case False{} _:
263 Lint.finding(Lint.Warning{}, True{}, span, alone(callee))
264 case True{} Some{p}:
265 Lint.finding(Lint.Information{}, True{}, span, slower(callee, p))
266 case True{} None{}:
267 []
268
269
270
271def banged(
272 info: Maybe<&2, Lint.Def>,
273 drop: Bool,
274 callee: String,
275 under: Set(),
276 slots: List<&2, Slot>,
277 span: Maybe<&2, Lint.Span>
278) -> List<&2, Lint.Finding>:
279 match info drop:
280 case Some{Lint.Def{t, params, forks, calls, r}} False{}:
281 banged.of(Sets.has(under, t), data(params, None{}, slots), callee, span)
282 case _ _:
283 []
284
285def judged(
286 bang: Bool,
287 info: Maybe<&2, Lint.Def>,
288 drop: Bool,
289 callee: String,
290 caller: Maybe<&2, Lint.Def>,
291 under: Set(),
292 slots: List<&2, Slot>,
293 span: Maybe<&2, Lint.Span>
294) -> List<&2, Lint.Finding>:
295 match bang:
296 case True{}:
297 banged(info, drop, callee, under, slots, span)
298 case False{}:
299 unbanged(info, callee, caller, slots, span)
300
301
302def visit(
303 +program: Program,
304 +path: List<&2, Lint.Above>,
305 +node: Lint.Node
306) -> List<&2, Lint.Finding>:
307 match program node:
308 case Program{
309 +defs,
310 under,
311 slots,
312 bangs
313 } Lint.Node{
314 Lint.Application{},
315 n,
316 span,
317 Con{Lint.Node{Lint.Reference{t}, callee, Some{Lint.Span{p, beg, end}}, k, f}, args},
318 facts
319 }:
320 judged(
321 List.contains(~U32, ~U32.is_eq, bangs, end),
322 Names.find(Lint.Def, defs, t),
323 is_drop(Names.find(Lint.Def, defs, t), t),
324 callee,
325 Names.find(Lint.Def, defs, Lint.Node.target(Lint.top(path, node))),
326 under,
327 slots,
328 span
329 )
330 case _ _:
331 []
332
333def run(+here: Lint.Here) -> Lint.Ask(List<&2, Lint.Finding>):
334 +sources = Lint.Here.sources(here)
335 +defs = Lint.Program.defs(sources, Map.new(&2, Maybe<&2, Lint.Def>))
336 Lint.done(
337 Lint.visit(
338 ~Program,
339 ~Lint.Finding,
340 ~visit,
341 Program{
342 defs,
343 program.under(sources, defs),
344 program.slots(sources, []),
345 bangs(Lint.Here.text(here), 0, [])
346 },
347 Lint.Here.nodes(here)
348 )
349 )
350
351def rule() -> Lint.Rule:
352 Lint.Rule{Lint.Needs{False{}, False{}}, [], Lint.OnFile{run}}