Skip to content

Commit c8c0495

Browse files
committed
compiler: super early exploration of instruction reordering
See comments in InstructionReordering.ts. This needs substantial iteration before landing in some form, just putting up to share for discussion. ghstack-source-id: 765c47c3d6ce37af06de307b71af5a8c2f4a8fa5 Pull Request resolved: #29579
1 parent 867edc6 commit c8c0495

File tree

59 files changed

+926
-800
lines changed

Some content is hidden

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

59 files changed

+926
-800
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

+4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
deadCodeElimination,
4242
pruneMaybeThrows,
4343
} from "../Optimization";
44+
import { instructionReordering } from "../Optimization/InstructionReordering";
4445
import {
4546
CodegenFunction,
4647
alignObjectMethodScopes,
@@ -195,6 +196,9 @@ function* runWithEnvironment(
195196
deadCodeElimination(hir);
196197
yield log({ kind: "hir", name: "DeadCodeElimination", value: hir });
197198

199+
instructionReordering(hir);
200+
yield log({ kind: "hir", name: "InstructionReordering", value: hir });
201+
198202
pruneMaybeThrows(hir);
199203
yield log({ kind: "hir", name: "PruneMaybeThrows", value: hir });
200204

compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts

+6
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,12 @@ class InferenceState {
737737
* For debugging purposes, dumps the state to a plain
738738
* object so that it can printed as JSON.
739739
*/
740+
inspect(): any {
741+
return {
742+
values: this.#values,
743+
variables: this.#variables,
744+
};
745+
}
740746
debug(): any {
741747
const result: any = { values: {}, variables: {} };
742748
const objects: Map<InstructionValue, number> = new Map();

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ function evaluateInstruction(
441441
}
442442
case "LoadLocal": {
443443
const placeValue = read(constants, value.place);
444-
if (placeValue !== null) {
444+
if (placeValue != null) {
445445
instr.value = placeValue;
446446
}
447447
return placeValue;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
import {
2+
BasicBlock,
3+
Environment,
4+
HIRFunction,
5+
IdentifierId,
6+
Instruction,
7+
markInstructionIds,
8+
} from "../HIR";
9+
import { printFunction, printInstruction } from "../HIR/PrintHIR";
10+
import {
11+
eachInstructionValueLValue,
12+
eachInstructionValueOperand,
13+
eachTerminalOperand,
14+
} from "../HIR/visitors";
15+
import { mayAllocate } from "../ReactiveScopes/InferReactiveScopeVariables";
16+
import { getOrInsertDefault } from "../Utils/utils";
17+
18+
/**
19+
* WIP early exploration of instruction reordering. This is a fairly aggressive form and has
20+
* some issues. The idea of what's implemented:
21+
*
22+
* The high-level approach is to build a dependency graph where nodes generally correspond
23+
* either to instructions OR to particular lvalue assignments of an expresssion. So
24+
* `Destructure [x, y] = z` creates 3 nodes: one for the instruction, and one each for x and y.
25+
* The lvalue nodes depend on the instruction node that assigns them.
26+
*
27+
* We add dependency edges for all the rvalues/lvalues of each instruction. In addition, we
28+
* implicitly add dependencies btw non-reorderable instructions (more on that criteria) to
29+
* serialize any instruction where order might be observable.
30+
*
31+
* We then distinguish two types of instructions that are reorderable:
32+
* - Primitives, JSXText, JSX elements, and globals can be *globally* reordered, ie across blocks.
33+
* We defer emitting them until they are first used globally.
34+
* - Array and object expressions are reorderable within basic blocks. This could likely be relaxed to be global.
35+
* - StoreLocal, LoadLocal, and Destructure are reorderable within basic blocks. However, we serialize all
36+
* references to each named variable (reads and writes) to ensure that we aren't changing the order of evaluation
37+
* of variable references.
38+
*
39+
* The variable reordering relies on the fact that any variables that could be reassigned via a function expression
40+
* are promoted to "context" variables and use LoadContext/StoreContext, which are not reorderable.
41+
*
42+
* In theory it might even be safe to do this variable reordering globally, but i want to think through that more.
43+
*
44+
* With the above context, the algorithm is approximately:
45+
* - For each basic block:
46+
* - Iterate the instructions to create the dependency graph
47+
* - Re-emit instructions, "pulling" from all the values that are depended upon by the block's terminal.
48+
* - Emit any remaining instructions that cannot be globally reordered, starting from later instructions first.
49+
* - Save any globally-reorderable instructions into a global map that is shared across blocks, so they can be
50+
* emitted by the first block that needs them.
51+
*
52+
* Emitting instructions is currently naive: we just iterate in the order that the dependencies were established.
53+
* If instruction 4 depends on instructions 1, 2, and 3, we'll visit in depth-first order and emit 1, 2, 3, 4.
54+
* That's true even if instruction 1 and 2 are simple instructions (for ex primitives) while instruction 3 has its
55+
* own large dependency tree.
56+
*
57+
* ## Issues/things to explore:
58+
*
59+
* - An obvious improvement is to weight the nodes and emit dependencies based on weight. Alternatively, we could try to
60+
* determine the reactive dependencies of each node, and try to emit nodes that have the same dependencies together.
61+
* - Reordering destructure statements means that we also end up deferring the evaluation of its RHS. So i noticed some
62+
* `const [state, setState] = useState(...)` getting moved around. But i think i might have just messed up the bit that
63+
* ensures non-reorderable instructions (like the useState() call here) are serialized. So this should just be a simple fix,
64+
* if i didn't already fix it (need to go back through the fixture output changes)
65+
* - I also noticed that destructuring being moved meant that some reactive scopes ended up with less precise input, because
66+
* the destructure moved into the reactive scope itself (so the scope depends on the rvalue of the destructure, not the lvalues).
67+
* This is weird, i need to debug.
68+
* - Probably more things.
69+
*/
70+
export function instructionReordering(fn: HIRFunction): void {
71+
DEBUG && console.log(printFunction(fn));
72+
const globalDependencies: Dependencies = new Map();
73+
for (const [, block] of fn.body.blocks) {
74+
reorderBlock(fn.env, block, globalDependencies);
75+
}
76+
markInstructionIds(fn.body);
77+
DEBUG && console.log(printFunction(fn));
78+
}
79+
80+
const DEBUG = false;
81+
82+
type Dependencies = Map<IdentifierId, Node>;
83+
type Node = {
84+
instruction: Instruction | null;
85+
dependencies: Array<IdentifierId>;
86+
depth: number | null;
87+
};
88+
89+
function reorderBlock(
90+
env: Environment,
91+
block: BasicBlock,
92+
globalDependencies: Dependencies
93+
): void {
94+
DEBUG && console.log(`bb${block.id}`);
95+
const dependencies: Dependencies = new Map();
96+
const locals = new Map<string, IdentifierId>();
97+
let previousIdentifier: IdentifierId | null = null;
98+
for (const instr of block.instructions) {
99+
const node: Node = getOrInsertDefault(
100+
dependencies,
101+
instr.lvalue.identifier.id,
102+
{
103+
instruction: instr,
104+
dependencies: [],
105+
depth: null,
106+
}
107+
);
108+
if (getReorderingLevel(instr) === ReorderingLevel.None) {
109+
if (previousIdentifier !== null) {
110+
node.dependencies.push(previousIdentifier);
111+
}
112+
previousIdentifier = instr.lvalue.identifier.id;
113+
}
114+
for (const operand of eachInstructionValueOperand(instr.value)) {
115+
if (
116+
operand.identifier.name !== null &&
117+
operand.identifier.name.kind === "named"
118+
) {
119+
const previous = locals.get(operand.identifier.name.value);
120+
if (previous !== undefined) {
121+
node.dependencies.push(previous);
122+
}
123+
locals.set(operand.identifier.name.value, instr.lvalue.identifier.id);
124+
} else {
125+
if (dependencies.has(operand.identifier.id) || globalDependencies.has(operand.identifier.id)) {
126+
node.dependencies.push(operand.identifier.id);
127+
}
128+
}
129+
}
130+
dependencies.set(instr.lvalue.identifier.id, node);
131+
132+
for (const lvalue of eachInstructionValueLValue(instr.value)) {
133+
const lvalueNode = getOrInsertDefault(
134+
dependencies,
135+
lvalue.identifier.id,
136+
{
137+
instruction: null,
138+
dependencies: [],
139+
depth: null,
140+
}
141+
);
142+
lvalueNode.dependencies.push(instr.lvalue.identifier.id);
143+
if (
144+
lvalue.identifier.name !== null &&
145+
lvalue.identifier.name.kind === "named"
146+
) {
147+
const previous = locals.get(lvalue.identifier.name.value);
148+
if (previous !== undefined) {
149+
node.dependencies.push(previous);
150+
}
151+
locals.set(lvalue.identifier.name.value, instr.lvalue.identifier.id);
152+
}
153+
}
154+
}
155+
156+
function getDepth(env: Environment, id: IdentifierId): number {
157+
const node = dependencies.get(id);
158+
if (node == null) {
159+
return 0;
160+
}
161+
if (node.depth !== null) {
162+
return node.depth;
163+
}
164+
node.depth = 0;
165+
let depth =
166+
node.instruction != null && mayAllocate(env, node.instruction) ? 1 : 0;
167+
for (const dep of node.dependencies) {
168+
depth += getDepth(env, dep);
169+
}
170+
node.depth = depth;
171+
return depth;
172+
}
173+
174+
const instructions: Array<Instruction> = [];
175+
176+
function print(
177+
id: IdentifierId,
178+
seen: Set<IdentifierId>,
179+
depth: number = 0
180+
): void {
181+
const node = dependencies.get(id) ?? globalDependencies.get(id);
182+
if (node == null || seen.has(id)) {
183+
DEBUG && console.log(`${"\t|".repeat(depth)} skip $${id}`);
184+
return;
185+
}
186+
seen.add(id);
187+
node.dependencies.sort((a, b) => {
188+
const aDepth = getDepth(env, a);
189+
const bDepth = getDepth(env, b);
190+
return bDepth - aDepth;
191+
});
192+
for (const dep of node.dependencies) {
193+
print(dep, seen, depth + 1);
194+
}
195+
DEBUG && console.log(`${"\t|".repeat(depth)} ${printNode(id, node)}`);
196+
}
197+
const seen = new Set<IdentifierId>();
198+
if (DEBUG) {
199+
for (const operand of eachTerminalOperand(block.terminal)) {
200+
print(operand.identifier.id, seen);
201+
}
202+
for (const id of Array.from(dependencies.keys()).reverse()) {
203+
print(id, seen);
204+
}
205+
}
206+
207+
function emit(id: IdentifierId): void {
208+
const node = dependencies.get(id) ?? globalDependencies.get(id);
209+
if (node == null) {
210+
return;
211+
}
212+
dependencies.delete(id);
213+
globalDependencies.delete(id);
214+
node.dependencies.sort((a, b) => {
215+
const aDepth = getDepth(env, a);
216+
const bDepth = getDepth(env, b);
217+
return bDepth - aDepth;
218+
});
219+
for (const dep of node.dependencies) {
220+
emit(dep);
221+
}
222+
if (node.instruction !== null) {
223+
instructions.push(node.instruction);
224+
}
225+
}
226+
227+
for (const operand of eachTerminalOperand(block.terminal)) {
228+
DEBUG && console.log(`terminal operand: $${operand.identifier.id}`);
229+
emit(operand.identifier.id);
230+
}
231+
/**
232+
* Gross hack: for value blocks we want the terminal operand to be emitted last, since that's its value.
233+
* For other blocks the exact order doesn't matter, we assume instructions whose values aren't depended
234+
* upon by the block terminal are used later, so it makes sense to order them last.
235+
*/
236+
const index = instructions.length;
237+
for (const id of Array.from(dependencies.keys()).reverse()) {
238+
const node = dependencies.get(id);
239+
if (node == null) {
240+
continue;
241+
}
242+
if (
243+
node.instruction !== null &&
244+
getReorderingLevel(node.instruction) === ReorderingLevel.Global &&
245+
(block.kind === 'block' || block.kind === 'catch')
246+
) {
247+
globalDependencies.set(id, node);
248+
DEBUG && console.log(`global: $${id}`);
249+
} else {
250+
DEBUG && console.log(`other: $${id}`);
251+
emit(id);
252+
}
253+
}
254+
if (block.kind !== 'block' && block.kind !== 'catch') {
255+
const extra = instructions.splice(index);
256+
instructions.splice(0, 0, ...extra);
257+
}
258+
block.instructions = instructions;
259+
DEBUG && console.log();
260+
}
261+
262+
function printDeps(deps: Dependencies): string {
263+
return (
264+
"[\n" +
265+
Array.from(deps)
266+
.map(([id, dep]) => printNode(id, dep))
267+
.join("\n") +
268+
"\n]"
269+
);
270+
}
271+
272+
function printNode(id: number, node: Node): string {
273+
if (
274+
node.instruction != null &&
275+
node.instruction.value.kind === "FunctionExpression"
276+
) {
277+
return `$${id} FunctionExpression deps=[${node.dependencies
278+
.map((x) => `$${x}`)
279+
.join(", ")}]`;
280+
}
281+
return `$${id} ${
282+
node.instruction != null ? printInstruction(node.instruction) : ""
283+
} deps=[${node.dependencies.map((x) => `$${x}`).join(", ")}] depth=${
284+
node.depth
285+
}`;
286+
}
287+
288+
enum ReorderingLevel {
289+
None = "none",
290+
Local = "local",
291+
Global = "global",
292+
}
293+
function getReorderingLevel(instr: Instruction): ReorderingLevel {
294+
switch (instr.value.kind) {
295+
case "JsxExpression":
296+
case "JsxFragment":
297+
case "JSXText":
298+
case "LoadGlobal":
299+
case "Primitive":
300+
case "TemplateLiteral": {
301+
return ReorderingLevel.Global;
302+
}
303+
/*
304+
For locals, a simple and robust strategy is to figure out the range of instructions where the identifier may be reassigned,
305+
and then allow reordering of LoadLocal instructions which occur after this range. Obviously for const, this means that all
306+
LoadLocals can be reordered, so a simpler thing to start with is just to only allow reordering of loads of known-consts.
307+
308+
With this overall strategy we can allow global reordering of LoadLocals and remove the global/local reordering distinction
309+
(all reordering can be global).
310+
311+
case "Destructure":
312+
case "StoreLocal":
313+
case "LoadLocal":
314+
{
315+
return ReorderingLevel.Local;
316+
}
317+
*/
318+
default: {
319+
return ReorderingLevel.None;
320+
}
321+
}
322+
}

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

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

168-
function mayAllocate(env: Environment, instruction: Instruction): boolean {
168+
export function mayAllocate(
169+
env: Environment,
170+
instruction: Instruction
171+
): boolean {
169172
const { value } = instruction;
170173
switch (value.kind) {
171174
case "Destructure": {

0 commit comments

Comments
 (0)