Skip to content

Commit 8f53b1e

Browse files
committed
[compiler] Optimize emission in normal (non-value) blocks
In #29863 I tried to find a clean way to share code for emitting instructions between value blocks and regular blocks. The catch is that value blocks have special meaning for their final instruction — that's the value of the block — so reordering can't change the last instruction. However, in finding a clean way to share code for these two categories of code, i also inadvertently reduced the effectiveness of the optimization. This PR updates to use different strategies for these two kinds of blocks: value blocks use the code from #29863 where we first emit all non-reorderable instructions in their original order, _then_ try to emit reorderable values. The reason this is suboptimal, though, is that we want to move instructions closer to their dependencies so that they can invalidate (merge) together. Emitting the reorderable values last prevents this. So for normal blocks, we now emit terminal operands first. This will invariably cause _some_ of the non-reorderable instructions to be emitted, but it will intersperse reoderable instructions in between, right after their dependencies. This maximizes our ability to merge scopes. I think the complexity cost of two strategies is worth the benefit, though i still need to double-check all the output changes. ghstack-source-id: 98824627b66f7a43aeaf141c21efddc60c3cc0b3 Pull Request resolved: #29883
1 parent f8cb40b commit 8f53b1e

File tree

149 files changed

+1386
-1219
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

149 files changed

+1386
-1219
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ const EnvironmentConfigSchema = z.object({
281281
* Enable instruction reordering. See InstructionReordering.ts for the details
282282
* of the approach.
283283
*/
284-
enableInstructionReordering: z.boolean().default(false),
284+
enableInstructionReordering: z.boolean().default(true),
285285

286286
/*
287287
* Enables instrumentation codegen. This emits a dev-mode only call to an

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

+118-60
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
eachInstructionValueOperand,
2727
eachTerminalOperand,
2828
} from "../HIR/visitors";
29-
import { mayAllocate } from "../ReactiveScopes/InferReactiveScopeVariables";
3029
import { getOrInsertWith } from "../Utils/utils";
3130

3231
/**
@@ -93,6 +92,7 @@ type Nodes = Map<IdentifierId, Node>;
9392
type Node = {
9493
instruction: Instruction | null;
9594
dependencies: Set<IdentifierId>;
95+
reorderability: Reorderability;
9696
depth: number | null;
9797
};
9898

@@ -173,21 +173,23 @@ function reorderBlock(
173173
for (const instr of block.instructions) {
174174
const { lvalue, value } = instr;
175175
// Get or create a node for this lvalue
176+
const reorderability = getReorderability(instr, references);
176177
const node = getOrInsertWith(
177178
locals,
178179
lvalue.identifier.id,
179180
() =>
180181
({
181182
instruction: instr,
182183
dependencies: new Set(),
184+
reorderability,
183185
depth: null,
184186
}) as Node
185187
);
186188
/**
187189
* Ensure non-reoderable instructions have their order retained by
188190
* adding explicit dependencies to the previous such instruction.
189191
*/
190-
if (getReoderability(instr, references) === Reorderability.Nonreorderable) {
192+
if (reorderability === Reorderability.Nonreorderable) {
191193
if (previous !== null) {
192194
node.dependencies.add(previous);
193195
}
@@ -243,67 +245,125 @@ function reorderBlock(
243245

244246
DEBUG && console.log(`bb${block.id}`);
245247

246-
// First emit everything that can't be reordered
247-
if (previous !== null) {
248-
DEBUG && console.log(`(last non-reorderable instruction)`);
249-
DEBUG && print(env, locals, shared, seen, previous);
250-
emit(env, locals, shared, nextInstructions, previous);
251-
}
252-
/*
253-
* For "value" blocks the final instruction represents its value, so we have to be
254-
* careful to not change the ordering. Emit the last instruction explicitly.
255-
* Any non-reorderable instructions will get emitted first, and any unused
256-
* reorderable instructions can be deferred to the shared node list.
248+
/**
249+
* The ideal order for emitting instructions may change the final instruction,
250+
* but value blocks have special semantics for the final instruction of a block -
251+
* that's the expression's value!. So we choose between a less optimal strategy
252+
* for value blocks which preserves the final instruction order OR a more optimal
253+
* ordering for statement-y blocks.
257254
*/
258-
if (isExpressionBlockKind(block.kind) && block.instructions.length !== 0) {
259-
DEBUG && console.log(`(block value)`);
260-
DEBUG &&
261-
print(
255+
if (isExpressionBlockKind(block.kind)) {
256+
// First emit everything that can't be reordered
257+
if (previous !== null) {
258+
DEBUG && console.log(`(last non-reorderable instruction)`);
259+
DEBUG && print(env, locals, shared, seen, previous);
260+
emit(env, locals, shared, nextInstructions, previous);
261+
}
262+
/*
263+
* For "value" blocks the final instruction represents its value, so we have to be
264+
* careful to not change the ordering. Emit the last instruction explicitly.
265+
* Any non-reorderable instructions will get emitted first, and any unused
266+
* reorderable instructions can be deferred to the shared node list.
267+
*/
268+
if (block.instructions.length !== 0) {
269+
DEBUG && console.log(`(block value)`);
270+
DEBUG &&
271+
print(
272+
env,
273+
locals,
274+
shared,
275+
seen,
276+
block.instructions.at(-1)!.lvalue.identifier.id
277+
);
278+
emit(
262279
env,
263280
locals,
264281
shared,
265-
seen,
282+
nextInstructions,
266283
block.instructions.at(-1)!.lvalue.identifier.id
267284
);
268-
emit(
269-
env,
270-
locals,
271-
shared,
272-
nextInstructions,
273-
block.instructions.at(-1)!.lvalue.identifier.id
274-
);
275-
}
276-
/*
277-
* Then emit the dependencies of the terminal operand. In many cases they will have
278-
* already been emitted in the previous step and this is a no-op.
279-
* TODO: sort the dependencies based on weight, like we do for other nodes. Not a big
280-
* deal though since most terminals have a single operand
281-
*/
282-
for (const operand of eachTerminalOperand(block.terminal)) {
283-
DEBUG && console.log(`(terminal operand)`);
284-
DEBUG && print(env, locals, shared, seen, operand.identifier.id);
285-
emit(env, locals, shared, nextInstructions, operand.identifier.id);
286-
}
287-
// Anything not emitted yet is globally reorderable
288-
for (const [id, node] of locals) {
289-
if (node.instruction == null) {
290-
continue;
291285
}
292-
CompilerError.invariant(
293-
node.instruction != null &&
294-
getReoderability(node.instruction, references) ===
295-
Reorderability.Reorderable,
296-
{
297-
reason: `Expected all remaining instructions to be reorderable`,
298-
loc: node.instruction?.loc ?? block.terminal.loc,
299-
description:
300-
node.instruction != null
301-
? `Instruction [${node.instruction.id}] was not emitted yet but is not reorderable`
302-
: `Lvalue $${id} was not emitted yet but is not reorderable`,
286+
/*
287+
* Then emit the dependencies of the terminal operand. In many cases they will have
288+
* already been emitted in the previous step and this is a no-op.
289+
* TODO: sort the dependencies based on weight, like we do for other nodes. Not a big
290+
* deal though since most terminals have a single operand
291+
*/
292+
for (const operand of eachTerminalOperand(block.terminal)) {
293+
DEBUG && console.log(`(terminal operand)`);
294+
DEBUG && print(env, locals, shared, seen, operand.identifier.id);
295+
emit(env, locals, shared, nextInstructions, operand.identifier.id);
296+
}
297+
// Anything not emitted yet is globally reorderable
298+
for (const [id, node] of locals) {
299+
if (node.instruction == null) {
300+
continue;
303301
}
304-
);
305-
DEBUG && console.log(`save shared: $${id}`);
306-
shared.set(id, node);
302+
CompilerError.invariant(
303+
node.reorderability === Reorderability.Reorderable,
304+
{
305+
reason: `Expected all remaining instructions to be reorderable`,
306+
loc: node.instruction?.loc ?? block.terminal.loc,
307+
description:
308+
node.instruction != null
309+
? `Instruction [${node.instruction.id}] was not emitted yet but is not reorderable`
310+
: `Lvalue $${id} was not emitted yet but is not reorderable`,
311+
}
312+
);
313+
314+
DEBUG && console.log(`save shared: $${id}`);
315+
shared.set(id, node);
316+
}
317+
} else {
318+
/**
319+
* If this is not a value block, then the order within the block doesn't matter
320+
* and we can optimize more. The observation is that blocks often have instructions
321+
* such as:
322+
*
323+
* ```
324+
* t$0 = nonreorderable
325+
* t$1 = nonreorderable <-- this gets in the way of merging t$0 and t$2
326+
* t$2 = reorderable deps[ t$0 ]
327+
* return t$2
328+
* ```
329+
*
330+
* Ie where there is some pair of nonreorderable+reorderable values, with some intervening
331+
* also non-reorderable instruction. If we emit all non-reorderable instructions first,
332+
* then we'll keep the original order. But reordering instructions don't just mean moving
333+
* them later: we can also move then _earlier_. By starting from terminal operands, we
334+
* end up emitting:
335+
*
336+
* ```
337+
* t$0 = nonreorderable // dep of t$2
338+
* t$2 = reorderable deps[ t$0 ]
339+
* t$1 = nonreorderable <-- not in the way of merging anymore!
340+
* return t$2
341+
* ```
342+
*
343+
* Ie all nonreorderable transitive deps of the terminal operands will get emitted first,
344+
* but we'll be able to intersperse the depending reorderable instructions in between
345+
* them in a way that works better with scope merging.
346+
*/
347+
for (const operand of eachTerminalOperand(block.terminal)) {
348+
DEBUG && console.log(`(terminal operand)`);
349+
DEBUG && print(env, locals, shared, seen, operand.identifier.id);
350+
emit(env, locals, shared, nextInstructions, operand.identifier.id);
351+
}
352+
// Anything not emitted yet is globally reorderable
353+
for (const id of Array.from(locals.keys()).reverse()) {
354+
const node = locals.get(id);
355+
if (node === undefined) {
356+
continue;
357+
}
358+
if (node.reorderability === Reorderability.Reorderable) {
359+
DEBUG && console.log(`save shared: $${id}`);
360+
shared.set(id, node);
361+
} else {
362+
DEBUG && console.log("leftover");
363+
DEBUG && print(env, locals, shared, seen, id);
364+
emit(env, locals, shared, nextInstructions, id);
365+
}
366+
}
307367
}
308368

309369
block.instructions = nextInstructions;
@@ -319,8 +379,7 @@ function getDepth(env: Environment, nodes: Nodes, id: IdentifierId): number {
319379
return node.depth;
320380
}
321381
node.depth = 0; // in case of cycles
322-
let depth =
323-
node.instruction != null && mayAllocate(env, node.instruction) ? 1 : 0;
382+
let depth = node.reorderability === Reorderability.Reorderable ? 1 : 10;
324383
for (const dep of node.dependencies) {
325384
depth += getDepth(env, nodes, dep);
326385
}
@@ -355,7 +414,7 @@ function print(
355414
print(env, locals, shared, seen, dep, depth + 1);
356415
}
357416
console.log(
358-
`${"| ".repeat(depth)}$${id} ${printNode(node)} deps=[${deps.map((x) => `$${x}`).join(", ")}]`
417+
`${"| ".repeat(depth)}$${id} ${printNode(node)} deps=[${deps.map((x) => `$${x}`).join(", ")}] depth=${node.depth}`
359418
);
360419
}
361420

@@ -406,7 +465,7 @@ enum Reorderability {
406465
Reorderable,
407466
Nonreorderable,
408467
}
409-
function getReoderability(
468+
function getReorderability(
410469
instr: Instruction,
411470
references: References
412471
): Reorderability {
@@ -432,7 +491,6 @@ function getReoderability(
432491
range !== undefined &&
433492
range.end === range.start // this LoadLocal is used exactly once
434493
) {
435-
console.log(`reorderable: ${name.value}`);
436494
return Reorderability.Reorderable;
437495
}
438496
}

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": {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/alias-computed-load.expect.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,20 @@ function component(a) {
1919
import { c as _c } from "react/compiler-runtime";
2020
function component(a) {
2121
const $ = _c(2);
22-
let x;
22+
let t0;
2323
if ($[0] !== a) {
24-
x = { a };
24+
const x = { a };
2525
const y = {};
2626

27+
t0 = x;
2728
y.x = x.a;
2829
mutate(y);
2930
$[0] = a;
30-
$[1] = x;
31+
$[1] = t0;
3132
} else {
32-
x = $[1];
33+
t0 = $[1];
3334
}
34-
return x;
35+
return t0;
3536
}
3637

3738
```

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/alias-nested-member-path-mutate.expect.md

+7-5
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,21 @@ function component() {
2020
import { c as _c } from "react/compiler-runtime";
2121
function component() {
2222
const $ = _c(1);
23-
let x;
23+
let t0;
2424
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
2525
const z = [];
2626
const y = {};
2727
y.z = z;
28-
x = {};
28+
const x = {};
2929
x.y = y;
30+
31+
t0 = x;
3032
mutate(x.y.z);
31-
$[0] = x;
33+
$[0] = t0;
3234
} else {
33-
x = $[0];
35+
t0 = $[0];
3436
}
35-
return x;
37+
return t0;
3638
}
3739

3840
```

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/alias-nested-member-path.expect.md

+7-5
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,20 @@ export const FIXTURE_ENTRYPOINT = {
2525
import { c as _c } from "react/compiler-runtime";
2626
function component() {
2727
const $ = _c(1);
28-
let x;
28+
let t0;
2929
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
3030
const z = [];
3131
const y = {};
3232
y.z = z;
33-
x = {};
33+
const x = {};
34+
35+
t0 = x;
3436
x.y = y;
35-
$[0] = x;
37+
$[0] = t0;
3638
} else {
37-
x = $[0];
39+
t0 = $[0];
3840
}
39-
return x;
41+
return t0;
4042
}
4143

4244
export const FIXTURE_ENTRYPOINT = {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-within-nested-valueblock-in-array.expect.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,12 @@ import { Stringify, identity, makeArray, mutate } from "shared-runtime";
5252
* handles this correctly.
5353
*/
5454
function Foo(t0) {
55-
const $ = _c(4);
55+
const $ = _c(3);
5656
const { cond1, cond2 } = t0;
57-
const arr = makeArray({ a: 2 }, 2, []);
5857
let t1;
59-
if ($[0] !== cond1 || $[1] !== cond2 || $[2] !== arr) {
58+
if ($[0] !== cond1 || $[1] !== cond2) {
59+
const arr = makeArray({ a: 2 }, 2, []);
60+
6061
t1 = cond1 ? (
6162
<>
6263
<div>{identity("foo")}</div>
@@ -65,10 +66,9 @@ function Foo(t0) {
6566
) : null;
6667
$[0] = cond1;
6768
$[1] = cond2;
68-
$[2] = arr;
69-
$[3] = t1;
69+
$[2] = t1;
7070
} else {
71-
t1 = $[3];
71+
t1 = $[2];
7272
}
7373
return t1;
7474
}

0 commit comments

Comments
 (0)