@samitouri / QOS-React-2 / commits / 4aa60d32b9

[patch][dce] Patch dce to have separate mark and sweep phases

--- Previously, our logic was something like: ```js fixed-point-loop { foreach instruction { mark referenced identifiers // assume that usages are always visited before declarations if (instruction is decl) { prune(instruction); } } foreach instruction { if not referenced { delete(instruction); } } ``` This contained a bug, as not all usages of a variable are guaranteed to be visited before its declaration. ```js // input let x = 0; while(x < 10) { x += 2; } return x; // hir entry: x$0 = 0 goto loop-test loop-test: x$1 = phi(x$0, x$2) if ... goto loop-body else goto fallthrough loop-body: x$2 = x$1 ... goto loop-test fallthrough: return x$1 ``` In this example,`x$2` is defined by `loop-body` and used by `loop-test`. Similarly, `x$1` is defined by `loop-test` and used by `loop-body`. --- TODO: trying to come up with more test fixtures

Mofei Zhang committed Jan 18, 2024 at 18:29 UTC 4aa60d32b954ce14ad84892f5b4f937df3e42e28
3 files changed +186 -67
compiler/packages/babel-plugin-react-forget/src/Optimization/DeadCodeElimination.ts
+91 -67
@@ -29,62 +29,17 @@ import { assertExhaustive, retainWhere } from "../Utils/utils";
29 * Note that unreachable blocks are already pruned during HIR construction.
30 */
31 export function deadCodeElimination(fn: HIRFunction): void {
32 - const state = new State();
33 -
34 - /*
35 - * If there are no back-edges the algorithm can terminate after a single iteration
36 - * of the blocks
32 + /**
33 + * Phase 1: Find/mark all referenced identifiers
34 + * Usages may be visited AFTER declarations if there are circular phi / data dependencies
35 + * between blocks, so we wait to sweep until after fixed point iteration is complete
36 */
38 - const hasLoop = hasBackEdge(fn);
39 -
40 - const reversedBlocks = [...fn.body.blocks.values()].reverse();
41 - let size = state.count;
42 - do {
43 - size = state.count;
44 -
45 - /*
46 - * Iterate blocks in postorder (successors before predecessors, excepting loops)
47 - * to find usages before declarations
48 - */
49 - for (const block of reversedBlocks) {
50 - for (const operand of eachTerminalOperand(block.terminal)) {
51 - state.reference(operand.identifier);
52 - }
53 -
54 - for (let i = block.instructions.length - 1; i >= 0; i--) {
55 - const instr = block.instructions[i]!;
56 - if (
57 - !state.isIdOrNameUsed(instr.lvalue.identifier) &&
58 - pruneableValue(instr.value, state) &&
59 - // Can't prune the last value of a value block, that's its value!
60 - !(block.kind !== "block" && i === block.instructions.length - 1)
61 - ) {
62 - continue;
63 - }
64 - state.reference(instr.lvalue.identifier);
37 + const state = findReferencedIdentifiers(fn);
38
66 - /*
67 - * For the last value of a value block, if it's not pruneable we can't
68 - * rewrite it. This is necessary to preserve unused value blocks
69 - */
70 - if (block.kind !== "block" && i === block.instructions.length - 1) {
71 - for (const place of eachInstructionValueOperand(instr.value)) {
72 - state.reference(place.identifier);
73 - }
74 - continue;
75 - }
76 - // Otherwise rewrite instructions to remove unused parts of them
77 - visitInstruction(instr, state);
78 - }
79 - for (const phi of block.phis) {
80 - if (state.isIdOrNameUsed(phi.id)) {
81 - for (const [_pred, operand] of phi.operands) {
82 - state.reference(operand);
83 - }
84 - }
85 - }
86 - }
87 - } while (state.count > size && hasLoop);
39 + /**
40 + * Phase 2: Prune / sweep unreferenced identifiers and instructions
41 + * as possible (subject to HIR structural constraints)
42 + */
43 for (const [, block] of fn.body.blocks) {
44 for (const phi of block.phis) {
45 if (!state.isIdOrNameUsed(phi.id)) {
@@ -94,6 +49,14 @@ export function deadCodeElimination(fn: HIRFunction): void {
49 retainWhere(block.instructions, (instr) =>
50 state.isIdOrNameUsed(instr.lvalue.identifier)
51 );
52 + // Rewrite retained instructions
53 + for (let i = 0; i < block.instructions.length; i++) {
54 + const isBlockValue =
55 + block.kind !== "block" && i === block.instructions.length - 1;
56 + if (!isBlockValue) {
57 + rewriteInstruction(block.instructions[i], state);
58 + }
59 + }
60 }
61 }
62
@@ -134,10 +97,81 @@ class State {
97 }
98 }
99
137 -function visitInstruction(instr: Instruction, state: State): void {
100 +function findReferencedIdentifiers(fn: HIRFunction): State {
101 + /*
102 + * If there are no back-edges the algorithm can terminate after a single iteration
103 + * of the blocks
104 + */
105 + const hasLoop = hasBackEdge(fn);
106 + const reversedBlocks = [...fn.body.blocks.values()].reverse();
107 +
108 + const state = new State();
109 + let size = state.count;
110 + do {
111 + size = state.count;
112 +
113 + /*
114 + * Iterate blocks in postorder (successors before predecessors, excepting loops)
115 + * to visit usages before declarations
116 + */
117 + for (const block of reversedBlocks) {
118 + for (const operand of eachTerminalOperand(block.terminal)) {
119 + state.reference(operand.identifier);
120 + }
121 +
122 + for (let i = block.instructions.length - 1; i >= 0; i--) {
123 + const instr = block.instructions[i]!;
124 + const isBlockValue =
125 + block.kind !== "block" && i === block.instructions.length - 1;
126 +
127 + if (isBlockValue) {
128 + /**
129 + * The last instr of a value block is never eligible for pruning,
130 + * as that's the block's value. Pessimistically consider all operands
131 + * as used to avoid rewriting the last instruction
132 + */
133 + state.reference(instr.lvalue.identifier);
134 + for (const place of eachInstructionValueOperand(instr.value)) {
135 + state.reference(place.identifier);
136 + }
137 + } else if (
138 + state.isIdOrNameUsed(instr.lvalue.identifier) ||
139 + !pruneableValue(instr.value, state)
140 + ) {
141 + state.reference(instr.lvalue.identifier);
142 +
143 + if (instr.value.kind === "StoreLocal") {
144 + /*
145 + * If this is a Let/Const declaration, mark the initializer as referenced
146 + * only if the ssa'ed lval is also referenced
147 + */
148 + if (
149 + instr.value.lvalue.kind === InstructionKind.Reassign ||
150 + state.isIdUsed(instr.value.lvalue.place.identifier)
151 + ) {
152 + state.reference(instr.value.value.identifier);
153 + }
154 + } else {
155 + for (const operand of eachInstructionValueOperand(instr.value)) {
156 + state.reference(operand.identifier);
157 + }
158 + }
159 + }
160 + }
161 + for (const phi of block.phis) {
162 + if (state.isIdOrNameUsed(phi.id)) {
163 + for (const [_pred, operand] of phi.operands) {
164 + state.reference(operand);
165 + }
166 + }
167 + }
168 + }
169 + } while (state.count > size && hasLoop);
170 + return state;
171 +}
172 +
173 +function rewriteInstruction(instr: Instruction, state: State): void {
174 if (instr.value.kind === "Destructure") {
139 - // Mark the value as used, not the lvalues
140 - state.reference(instr.value.value.identifier);
175 // Remove unused lvalues
176 switch (instr.value.lvalue.pattern.kind) {
177 case "ArrayPattern": {
@@ -219,16 +253,6 @@ function visitInstruction(instr: Instruction, state: State): void {
253 lvalue: instr.value.lvalue,
254 loc: instr.value.loc,
255 };
222 - } else {
223 - /*
224 - * Else we mark the initializer as referenced, since the variable itself is
225 - * referenced
226 - */
227 - state.reference(instr.value.value.identifier);
228 - }
229 - } else {
230 - for (const operand of eachInstructionValueOperand(instr.value)) {
231 - state.reference(operand.identifier);
256 }
257 }
258 }
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-dce-circular-reference.expect.md new
+72
@@ -0,0 +1,72 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { identity } from "shared-runtime";
6 +
7 +function Component({ data }) {
8 + let x = 0;
9 + for (const item of data) {
10 + const { current, other } = item;
11 + x += current;
12 + identity(other);
13 + }
14 + return [x];
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [
20 + {
21 + data: [
22 + { current: 2, other: 3 },
23 + { current: 4, other: 5 },
24 + ],
25 + },
26 + ],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { unstable_useMemoCache as useMemoCache } from "react";
35 +import { identity } from "shared-runtime";
36 +
37 +function Component(t25) {
38 + const $ = useMemoCache(2);
39 + const { data } = t25;
40 + let x = 0;
41 + for (const item of data) {
42 + const { current, other } = item;
43 + x = x + current;
44 + identity(other);
45 + }
46 + let t0;
47 + if ($[0] !== x) {
48 + t0 = [x];
49 + $[0] = x;
50 + $[1] = t0;
51 + } else {
52 + t0 = $[1];
53 + }
54 + return t0;
55 +}
56 +
57 +export const FIXTURE_ENTRYPOINT = {
58 + fn: Component,
59 + params: [
60 + {
61 + data: [
62 + { current: 2, other: 3 },
63 + { current: 4, other: 5 },
64 + ],
65 + },
66 + ],
67 +};
68 +
69 +```
70 +
71 +### Eval output
72 +(kind: ok) [6]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-dce-circular-reference.js new
+23
@@ -0,0 +1,23 @@
1 +import { identity } from "shared-runtime";
2 +
3 +function Component({ data }) {
4 + let x = 0;
5 + for (const item of data) {
6 + const { current, other } = item;
7 + x += current;
8 + identity(other);
9 + }
10 + return [x];
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [
16 + {
17 + data: [
18 + { current: 2, other: 3 },
19 + { current: 4, other: 5 },
20 + ],
21 + },
22 + ],
23 +};