Skip to content

Commit fb19d9f

Browse files
committed
Update
[ghstack-poisoned]
2 parents d6db84f + 8f0d145 commit fb19d9f

File tree

6 files changed

+400
-122
lines changed

6 files changed

+400
-122
lines changed

compiler/packages/babel-plugin-react-compiler/src/Optimization/InstructionReordering.ts

+213-65
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@ import {
1313
HIRFunction,
1414
IdentifierId,
1515
Instruction,
16+
InstructionId,
17+
Place,
1618
isExpressionBlockKind,
19+
makeInstructionId,
1720
markInstructionIds,
1821
} from "../HIR";
1922
import { printInstruction } from "../HIR/PrintHIR";
2023
import {
24+
eachInstructionLValue,
2125
eachInstructionValueLValue,
2226
eachInstructionValueOperand,
2327
eachTerminalOperand,
2428
} from "../HIR/visitors";
25-
import { mayAllocate } from "../ReactiveScopes/InferReactiveScopeVariables";
2629
import { getOrInsertWith } from "../Utils/utils";
2730

2831
/**
@@ -69,8 +72,9 @@ import { getOrInsertWith } from "../Utils/utils";
6972
export function instructionReordering(fn: HIRFunction): void {
7073
// Shared nodes are emitted when they are first used
7174
const shared: Nodes = new Map();
75+
const references = findReferencedRangeOfTemporaries(fn);
7276
for (const [, block] of fn.body.blocks) {
73-
reorderBlock(fn.env, block, shared);
77+
reorderBlock(fn.env, block, shared, references);
7478
}
7579
CompilerError.invariant(shared.size === 0, {
7680
reason: `InstructionReordering: expected all reorderable nodes to have been emitted`,
@@ -88,35 +92,103 @@ type Nodes = Map<IdentifierId, Node>;
8892
type Node = {
8993
instruction: Instruction | null;
9094
dependencies: Set<IdentifierId>;
95+
reorderability: Reorderability;
9196
depth: number | null;
9297
};
9398

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+
94163
function reorderBlock(
95164
env: Environment,
96165
block: BasicBlock,
97-
shared: Nodes
166+
shared: Nodes,
167+
references: References
98168
): void {
99169
const locals: Nodes = new Map();
100170
const named: Map<string, IdentifierId> = new Map();
101171
let previous: IdentifierId | null = null;
102172
for (const instr of block.instructions) {
103173
const { lvalue, value } = instr;
104174
// Get or create a node for this lvalue
175+
const reorderability = getReorderability(instr, references);
105176
const node = getOrInsertWith(
106177
locals,
107178
lvalue.identifier.id,
108179
() =>
109180
({
110181
instruction: instr,
111182
dependencies: new Set(),
183+
reorderability,
112184
depth: null,
113185
}) as Node
114186
);
115187
/**
116188
* Ensure non-reoderable instructions have their order retained by
117189
* adding explicit dependencies to the previous such instruction.
118190
*/
119-
if (getReoderability(instr) === Reorderability.Nonreorderable) {
191+
if (reorderability === Reorderability.Nonreorderable) {
120192
if (previous !== null) {
121193
node.dependencies.add(previous);
122194
}
@@ -172,66 +244,125 @@ function reorderBlock(
172244

173245
DEBUG && console.log(`bb${block.id}`);
174246

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.
186253
*/
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(
191278
env,
192279
locals,
193280
shared,
194-
seen,
281+
nextInstructions,
195282
block.instructions.at(-1)!.lvalue.identifier.id
196283
);
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;
220284
}
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;
231300
}
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+
}
235366
}
236367

237368
block.instructions = nextInstructions;
@@ -247,8 +378,7 @@ function getDepth(env: Environment, nodes: Nodes, id: IdentifierId): number {
247378
return node.depth;
248379
}
249380
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;
252382
for (const dep of node.dependencies) {
253383
depth += getDepth(env, nodes, dep);
254384
}
@@ -265,7 +395,7 @@ function print(
265395
depth: number = 0
266396
): void {
267397
if (seen.has(id)) {
268-
console.log(`${"| ".repeat(depth)}$${id} <skipped>`);
398+
DEBUG && console.log(`${"| ".repeat(depth)}$${id} <skipped>`);
269399
return;
270400
}
271401
seen.add(id);
@@ -282,11 +412,12 @@ function print(
282412
for (const dep of deps) {
283413
print(env, locals, shared, seen, dep, depth + 1);
284414
}
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+
);
290421
}
291422

292423
function printNode(node: Node): string {
@@ -336,7 +467,10 @@ enum Reorderability {
336467
Reorderable,
337468
Nonreorderable,
338469
}
339-
function getReoderability(instr: Instruction): Reorderability {
470+
function getReorderability(
471+
instr: Instruction,
472+
references: References
473+
): Reorderability {
340474
switch (instr.value.kind) {
341475
case "JsxExpression":
342476
case "JsxFragment":
@@ -348,6 +482,20 @@ function getReoderability(instr: Instruction): Reorderability {
348482
case "UnaryExpression": {
349483
return Reorderability.Reorderable;
350484
}
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+
}
351499
default: {
352500
return Reorderability.Nonreorderable;
353501
}

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,7 @@ export function isMutable({ id }: Instruction, place: Place): boolean {
186186
return id >= range.start && id < range.end;
187187
}
188188

189-
export function mayAllocate(
190-
env: Environment,
191-
instruction: Instruction
192-
): boolean {
189+
function mayAllocate(env: Environment, instruction: Instruction): boolean {
193190
const { value } = instruction;
194191
switch (value.kind) {
195192
case "Destructure": {

0 commit comments

Comments
 (0)