13
HIRFunction,
14
IdentifierId,
15
Instruction,
16
+ InstructionId,
17
+ Place,
18
isExpressionBlockKind,
19
+ makeInstructionId,
20
markInstructionIds,
21
} from "../HIR";
22
import { printInstruction } from "../HIR/PrintHIR";
23
import {
24
+ eachInstructionLValue,
25
eachInstructionValueLValue,
26
eachInstructionValueOperand,
27
eachTerminalOperand,
28
} from "../HIR/visitors";
25
-import { mayAllocate } from "../ReactiveScopes/InferReactiveScopeVariables";
29
import { getOrInsertWith } from "../Utils/utils";
30
31
/**
72
export function instructionReordering(fn: HIRFunction): void {
73
// Shared nodes are emitted when they are first used
74
const shared: Nodes = new Map();
75
+ const references = findReferencedRangeOfTemporaries(fn);
76
for (const [, block] of fn.body.blocks) {
73
- reorderBlock(fn.env, block, shared);
77
+ reorderBlock(fn.env, block, shared, references);
78
}
79
CompilerError.invariant(shared.size === 0, {
80
reason: `InstructionReordering: expected all reorderable nodes to have been emitted`,
92
type Node = {
93
instruction: Instruction | null;
94
dependencies: Set<IdentifierId>;
95
+ reorderability: Reorderability;
96
depth: number | null;
97
};
98
99
+// Inclusive start and end
100
+type References = {
101
+ singleUseIdentifiers: SingleUseIdentifiers;
102
+ lastAssignments: LastAssignments;
103
+};
104
+type LastAssignments = Map<string, InstructionId>;
105
+type SingleUseIdentifiers = Set<IdentifierId>;
106
+enum ReferenceKind {
107
+ Read,
108
+ Write,
109
+}
110
+function findReferencedRangeOfTemporaries(fn: HIRFunction): References {
111
+ const singleUseIdentifiers = new Map<IdentifierId, number>();
112
+ const lastAssignments: LastAssignments = new Map();
113
+ function reference(
114
+ instr: InstructionId,
115
+ place: Place,
116
+ kind: ReferenceKind
117
+ ): void {
118
+ if (
119
+ place.identifier.name !== null &&
120
+ place.identifier.name.kind === "named"
121
+ ) {
122
+ if (kind === ReferenceKind.Write) {
123
+ const name = place.identifier.name.value;
124
+ const previous = lastAssignments.get(name);
125
+ if (previous === undefined) {
126
+ lastAssignments.set(name, instr);
127
+ } else {
128
+ lastAssignments.set(
129
+ name,
130
+ makeInstructionId(Math.max(previous, instr))
131
+ );
132
+ }
133
+ }
134
+ return;
135
+ } else if (kind === ReferenceKind.Read) {
136
+ const previousCount = singleUseIdentifiers.get(place.identifier.id) ?? 0;
137
+ singleUseIdentifiers.set(place.identifier.id, previousCount + 1);
138
+ }
139
+ }
140
+ for (const [, block] of fn.body.blocks) {
141
+ for (const instr of block.instructions) {
142
+ for (const operand of eachInstructionValueLValue(instr.value)) {
143
+ reference(instr.id, operand, ReferenceKind.Read);
144
+ }
145
+ for (const lvalue of eachInstructionLValue(instr)) {
146
+ reference(instr.id, lvalue, ReferenceKind.Write);
147
+ }
148
+ }
149
+ for (const operand of eachTerminalOperand(block.terminal)) {
150
+ reference(block.terminal.id, operand, ReferenceKind.Read);
151
+ }
152
+ }
153
+ return {
154
+ singleUseIdentifiers: new Set(
155
+ [...singleUseIdentifiers]
156
+ .filter(([, count]) => count === 1)
157
+ .map(([id]) => id)
158
+ ),
159
+ lastAssignments,
160
+ };
161
+}
162
+
163
function reorderBlock(
164
env: Environment,
165
block: BasicBlock,
97
- shared: Nodes
166
+ shared: Nodes,
167
+ references: References
168
): void {
169
const locals: Nodes = new Map();
170
const named: Map<string, IdentifierId> = new Map();
172
for (const instr of block.instructions) {
173
const { lvalue, value } = instr;
174
// Get or create a node for this lvalue
175
+ const reorderability = getReorderability(instr, references);
176
const node = getOrInsertWith(
177
locals,
178
lvalue.identifier.id,
180
({
181
instruction: instr,
182
dependencies: new Set(),
183
+ reorderability,
184
depth: null,
185
}) as Node
186
);
188
* Ensure non-reoderable instructions have their order retained by
189
* adding explicit dependencies to the previous such instruction.
190
*/
119
- if (getReoderability(instr) === Reorderability.Nonreorderable) {
191
+ if (reorderability === Reorderability.Nonreorderable) {
192
if (previous !== null) {
193
node.dependencies.add(previous);
194
}
244
245
DEBUG && console.log(`bb${block.id}`);
246
175
- // First emit everything that can't be reordered
176
- if (previous !== null) {
177
- DEBUG && console.log(`(last non-reorderable instruction)`);
178
- DEBUG && print(env, locals, shared, seen, previous);
179
- emit(env, locals, shared, nextInstructions, previous);
180
- }
181
- /*
182
- * For "value" blocks the final instruction represents its value, so we have to be
183
- * careful to not change the ordering. Emit the last instruction explicitly.
184
- * Any non-reorderable instructions will get emitted first, and any unused
185
- * reorderable instructions can be deferred to the shared node list.
247
+ /**
248
+ * The ideal order for emitting instructions may change the final instruction,
249
+ * but value blocks have special semantics for the final instruction of a block -
250
+ * that's the expression's value!. So we choose between a less optimal strategy
251
+ * for value blocks which preserves the final instruction order OR a more optimal
252
+ * ordering for statement-y blocks.
253
*/
187
- if (isExpressionBlockKind(block.kind) && block.instructions.length !== 0) {
188
- DEBUG && console.log(`(block value)`);
189
- DEBUG &&
190
- print(
254
+ if (isExpressionBlockKind(block.kind)) {
255
+ // First emit everything that can't be reordered
256
+ if (previous !== null) {
257
+ DEBUG && console.log(`(last non-reorderable instruction)`);
258
+ DEBUG && print(env, locals, shared, seen, previous);
259
+ emit(env, locals, shared, nextInstructions, previous);
260
+ }
261
+ /*
262
+ * For "value" blocks the final instruction represents its value, so we have to be
263
+ * careful to not change the ordering. Emit the last instruction explicitly.
264
+ * Any non-reorderable instructions will get emitted first, and any unused
265
+ * reorderable instructions can be deferred to the shared node list.
266
+ */
267
+ if (block.instructions.length !== 0) {
268
+ DEBUG && console.log(`(block value)`);
269
+ DEBUG &&
270
+ print(
271
+ env,
272
+ locals,
273
+ shared,
274
+ seen,
275
+ block.instructions.at(-1)!.lvalue.identifier.id
276
+ );
277
+ emit(
278
env,
279
locals,
280
shared,
194
- seen,
281
+ nextInstructions,
282
block.instructions.at(-1)!.lvalue.identifier.id
283
);
197
- emit(
198
- env,
199
- locals,
200
- shared,
201
- nextInstructions,
202
- block.instructions.at(-1)!.lvalue.identifier.id
203
- );
204
- }
205
- /*
206
- * Then emit the dependencies of the terminal operand. In many cases they will have
207
- * already been emitted in the previous step and this is a no-op.
208
- * TODO: sort the dependencies based on weight, like we do for other nodes. Not a big
209
- * deal though since most terminals have a single operand
210
- */
211
- for (const operand of eachTerminalOperand(block.terminal)) {
212
- DEBUG && console.log(`(terminal operand)`);
213
- DEBUG && print(env, locals, shared, seen, operand.identifier.id);
214
- emit(env, locals, shared, nextInstructions, operand.identifier.id);
215
- }
216
- // Anything not emitted yet is globally reorderable
217
- for (const [id, node] of locals) {
218
- if (node.instruction == null) {
219
- continue;
284
}
221
- CompilerError.invariant(
222
- node.instruction != null &&
223
- getReoderability(node.instruction) === Reorderability.Reorderable,
224
- {
225
- reason: `Expected all remaining instructions to be reorderable`,
226
- loc: node.instruction?.loc ?? block.terminal.loc,
227
- description:
228
- node.instruction != null
229
- ? `Instruction [${node.instruction.id}] was not emitted yet but is not reorderable`
230
- : `Lvalue $${id} was not emitted yet but is not reorderable`,
285
+ /*
286
+ * Then emit the dependencies of the terminal operand. In many cases they will have
287
+ * already been emitted in the previous step and this is a no-op.
288
+ * TODO: sort the dependencies based on weight, like we do for other nodes. Not a big
289
+ * deal though since most terminals have a single operand
290
+ */
291
+ for (const operand of eachTerminalOperand(block.terminal)) {
292
+ DEBUG && console.log(`(terminal operand)`);
293
+ DEBUG && print(env, locals, shared, seen, operand.identifier.id);
294
+ emit(env, locals, shared, nextInstructions, operand.identifier.id);
295
+ }
296
+ // Anything not emitted yet is globally reorderable
297
+ for (const [id, node] of locals) {
298
+ if (node.instruction == null) {
299
+ continue;
300
}
232
- );
233
- DEBUG && console.log(`save shared: $${id}`);
234
- shared.set(id, node);
301
+ CompilerError.invariant(
302
+ node.reorderability === Reorderability.Reorderable,
303
+ {
304
+ reason: `Expected all remaining instructions to be reorderable`,
305
+ loc: node.instruction?.loc ?? block.terminal.loc,
306
+ description:
307
+ node.instruction != null
308
+ ? `Instruction [${node.instruction.id}] was not emitted yet but is not reorderable`
309
+ : `Lvalue $${id} was not emitted yet but is not reorderable`,
310
+ }
311
+ );
312
+
313
+ DEBUG && console.log(`save shared: $${id}`);
314
+ shared.set(id, node);
315
+ }
316
+ } else {
317
+ /**
318
+ * If this is not a value block, then the order within the block doesn't matter
319
+ * and we can optimize more. The observation is that blocks often have instructions
320
+ * such as:
321
+ *
322
+ * ```
323
+ * t$0 = nonreorderable
324
+ * t$1 = nonreorderable <-- this gets in the way of merging t$0 and t$2
325
+ * t$2 = reorderable deps[ t$0 ]
326
+ * return t$2
327
+ * ```
328
+ *
329
+ * Ie where there is some pair of nonreorderable+reorderable values, with some intervening
330
+ * also non-reorderable instruction. If we emit all non-reorderable instructions first,
331
+ * then we'll keep the original order. But reordering instructions doesn't just mean moving
332
+ * them later: we can also move them _earlier_. By starting from terminal operands we
333
+ * end up emitting:
334
+ *
335
+ * ```
336
+ * t$0 = nonreorderable // dep of t$2
337
+ * t$2 = reorderable deps[ t$0 ]
338
+ * t$1 = nonreorderable <-- not in the way of merging anymore!
339
+ * return t$2
340
+ * ```
341
+ *
342
+ * Ie all nonreorderable transitive deps of the terminal operands will get emitted first,
343
+ * but we'll be able to intersperse the depending reorderable instructions in between
344
+ * them in a way that works better with scope merging.
345
+ */
346
+ for (const operand of eachTerminalOperand(block.terminal)) {
347
+ DEBUG && console.log(`(terminal operand)`);
348
+ DEBUG && print(env, locals, shared, seen, operand.identifier.id);
349
+ emit(env, locals, shared, nextInstructions, operand.identifier.id);
350
+ }
351
+ // Anything not emitted yet is globally reorderable
352
+ for (const id of Array.from(locals.keys()).reverse()) {
353
+ const node = locals.get(id);
354
+ if (node === undefined) {
355
+ continue;
356
+ }
357
+ if (node.reorderability === Reorderability.Reorderable) {
358
+ DEBUG && console.log(`save shared: $${id}`);
359
+ shared.set(id, node);
360
+ } else {
361
+ DEBUG && console.log("leftover");
362
+ DEBUG && print(env, locals, shared, seen, id);
363
+ emit(env, locals, shared, nextInstructions, id);
364
+ }
365
+ }
366
}
367
368
block.instructions = nextInstructions;
378
return node.depth;
379
}
380
node.depth = 0; // in case of cycles
250
- let depth =
251
- node.instruction != null && mayAllocate(env, node.instruction) ? 1 : 0;
381
+ let depth = node.reorderability === Reorderability.Reorderable ? 1 : 10;
382
for (const dep of node.dependencies) {
383
depth += getDepth(env, nodes, dep);
384
}
395
depth: number = 0
396
): void {
397
if (seen.has(id)) {
268
- console.log(`${"| ".repeat(depth)}$${id} <skipped>`);
398
+ DEBUG && console.log(`${"| ".repeat(depth)}$${id} <skipped>`);
399
return;
400
}
401
seen.add(id);
412
for (const dep of deps) {
413
print(env, locals, shared, seen, dep, depth + 1);
414
}
285
- console.log(
286
- `${"| ".repeat(depth)}$${id} ${printNode(node)} deps=[${deps
287
- .map((x) => `$${x}`)
288
- .join(", ")}]`
289
- );
415
+ DEBUG &&
416
+ console.log(
417
+ `${"| ".repeat(depth)}$${id} ${printNode(node)} deps=[${deps
418
+ .map((x) => `$${x}`)
419
+ .join(", ")}] depth=${node.depth}`
420
+ );
421
}
422
423
function printNode(node: Node): string {
467
Reorderable,
468
Nonreorderable,
469
}
339
-function getReoderability(instr: Instruction): Reorderability {
470
+function getReorderability(
471
+ instr: Instruction,
472
+ references: References
473
+): Reorderability {
474
switch (instr.value.kind) {
475
case "JsxExpression":
476
case "JsxFragment":
482
case "UnaryExpression": {
483
return Reorderability.Reorderable;
484
}
485
+ case "LoadLocal": {
486
+ const name = instr.value.place.identifier.name;
487
+ if (name !== null && name.kind === "named") {
488
+ const lastAssignment = references.lastAssignments.get(name.value);
489
+ if (
490
+ lastAssignment !== undefined &&
491
+ lastAssignment < instr.id &&
492
+ references.singleUseIdentifiers.has(instr.lvalue.identifier.id)
493
+ ) {
494
+ return Reorderability.Reorderable;
495
+ }
496
+ }
497
+ return Reorderability.Nonreorderable;
498
+ }
499
default: {
500
return Reorderability.Nonreorderable;
501
}