Initial pass at dead code elimination

This is a first pass at DCE without having read any literate on the subject, so 
lemme know if there's a better approach. That said the algorithm is: 

* Keep a `Set<Identifier>` of identifiers that are used (and whose constructing 
logic cannot be removed). 

* Do a first RPO iteration of all block's phis. Any phi operand that 
participates in a loop is preemptively marked as "used" even if it isn't 
strictly used somewhere. This step is necessary bc these operands may otherwise 
not be used. 

* Do a second post-order iteration of all blocks, including iterating first 
their terminals, then reverse iteration of instructions, then their phis. Mark 
the operands of each as used as we encounter them, and prune instructions whose 
lvalue is never used. 

For now I was conservative about which types of instructions can be pruned. For 
example, call instructions are never pruned, even if the result of the call is 
never used. 

However one catch is that we currently prune instructions that cause values to 
become frozen. We had planned to add runtime calls (in dev) to freeze values for 
runtime enforcement, and if we want to do that we can always add these 
instructions back (or replace them with explicit freeze calls). 

There are a few potential next steps but we should discuss whether they're worth 
it: 

* Use fixpoint iteration to find exactly which operands are actually used. This 
would allow us to to prune cases such as `let x = 0; while (...) { x += 1 }` eg 
where there's a phi but the result is never used. Such cases should be rare in 
practice though. 

* Eliminate more types of instructions, eg eliminate function calls that don't 
have any mutable arguments.
This commit is contained in:
Joe Savona
2023-02-03 14:55:49 -08:00
parent 812e0ce701
commit 19e21b73a7
41 changed files with 230 additions and 176 deletions
+5 -2
View File
@@ -6,7 +6,6 @@
*/
import { NodePath } from "@babel/traverse";
import * as t from "@babel/types";
import { CompilerError } from "./CompilerError";
import {
HIRFunction,
lower,
@@ -18,7 +17,7 @@ import {
inferMutableRanges,
inferReferenceEffects,
} from "./Inference";
import { constantPropagation } from "./Optimization";
import { constantPropagation, deadCodeElimination } from "./Optimization";
import {
alignReactiveScopesToBlockScopes,
buildReactiveBlocks,
@@ -71,6 +70,10 @@ export function* run(
inferReferenceEffects(hir);
yield log({ kind: "hir", name: "InferReferenceEffects", value: hir });
// Note: Has to come after infer reference effects because "dead" code may still affect inference
deadCodeElimination(hir);
yield log({ kind: "hir", name: "DeadCodeElimination", value: hir });
inferMutableRanges(hir);
yield log({ kind: "hir", name: "InferMutableRanges", value: hir });
@@ -112,7 +112,7 @@ function applyConstantPropagation(fn: HIRFunction): boolean {
switch (terminal.kind) {
case "if": {
const testValue = read(constants, terminal.test);
if (testValue !== null && testValue.kind === "Primitive") {
if (testValue !== null) {
hasChanges = true;
const targetBlockId = Boolean(testValue.value)
? terminal.consequent
@@ -0,0 +1,140 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
BlockId,
HIRFunction,
Identifier,
Instruction,
InstructionValue,
} from "../HIR";
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from "../HIR/visitors";
import { assertExhaustive } from "../Utils/utils";
/**
* Implements dead-code elimination, eliminating instructions whose values are unused.
*
* Note that unreachable blocks are already pruned during HIR construction.
*/
export function deadCodeElimination(fn: HIRFunction): void {
const used = new Set<Identifier>();
// Find any phi operands involved in a loop, which might otherwise appear as dead
// code when using a reverse iteration.
//
// A more advanced algorithm could still prune some of these operands
// let's keep it simple for now
const seen = new Set<BlockId>();
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
for (const [pred, operand] of phi.operands) {
if (!seen.has(pred)) {
used.add(operand);
}
}
}
seen.add(block.id);
}
// Iterate blocks in postorder (successors before predecessors, excepting loops)
// to find usages before declarations
const reversedBlocks = [...fn.body.blocks.values()].reverse();
for (const block of reversedBlocks) {
for (const operand of eachTerminalOperand(block.terminal)) {
used.add(operand.identifier);
}
let nextInstructions: Array<Instruction> | null = null;
for (let i = block.instructions.length - 1; i >= 0; i--) {
const instr = block.instructions[i]!;
if (
!used.has(instr.lvalue.place.identifier) &&
pruneableValue(instr.value) &&
// Can't prune the last value of a value block, that's its value!
!(block.kind !== "block" && i === block.instructions.length - 1)
) {
continue;
}
used.add(instr.lvalue.place.identifier);
nextInstructions ??= [];
nextInstructions.push(instr);
for (const operand of eachInstructionValueOperand(instr.value)) {
used.add(operand.identifier);
}
}
if (nextInstructions !== null) {
nextInstructions.reverse();
block.instructions = nextInstructions;
}
for (const phi of block.phis) {
if (used.has(phi.id)) {
for (const [, operand] of phi.operands) {
used.add(operand);
}
} else {
for (const [, operand] of phi.operands) {
if (used.has(operand)) {
used.add(phi.id);
for (const [, operand] of phi.operands) {
used.add(operand);
}
break;
}
}
}
}
}
}
/**
* Returns true if it is safe to prune an instruction with the given value.
* Functions which may have side-
*/
function pruneableValue(value: InstructionValue): boolean {
switch (value.kind) {
case "CallExpression":
case "ComputedCall":
case "ComputedStore":
case "PropertyCall":
case "PropertyStore": {
// Mutating instructions are not safe to prune.
// TODO: we could be more precise and make this conditional on whether
// any arguments are actually modified
return false;
}
case "NewExpression":
case "UnsupportedNode":
case "TaggedTemplateExpression": {
// Potentially safe to prune, since they should just be creating new values
return false;
}
case "ArrayExpression":
case "BinaryExpression":
case "ComputedLoad":
case "ComputedStore":
case "FunctionExpression":
case "Identifier":
case "JsxExpression":
case "JsxFragment":
case "JSXText":
case "ObjectExpression":
case "Primitive":
case "PropertyLoad":
case "TemplateLiteral":
case "TypeCastExpression":
case "UnaryExpression": {
// Definitely safe to prune since they are read-only
return true;
}
default: {
assertExhaustive(value, `Unexepcted value kind '${(value as any).kind}'`);
}
}
}
@@ -6,3 +6,4 @@
*/
export { constantPropagation } from "./ConstantPropagation";
export { deadCodeElimination } from "./DeadCodeElimination";
@@ -15,10 +15,6 @@ function f() {
```javascript
function f() {
const x = 1;
const x$0 = 2;
4;
return 6;
}
@@ -28,7 +28,7 @@ function Component(props) {
a = [];
b = {};
foo(a, b);
const _ = <div a={a}></div>;
foo(b);
$[0] = a;
$[1] = b;
@@ -29,19 +29,18 @@ function component(a) {
} else {
z = $[1];
}
const x = undefined;
const c_2 = $[2] !== z;
let x$0;
let x;
if (c_2) {
x$0 = function () {
x = function () {
z;
};
$[2] = z;
$[3] = x$0;
$[3] = x;
} else {
x$0 = $[3];
x = $[3];
}
return x$0;
return x;
}
```
@@ -22,7 +22,6 @@ function component() {
} else {
t0 = $[0];
}
const x = t0[0];
const setX = t0[1];
let handler;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
@@ -20,7 +20,7 @@ function foo() {
let y;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
y = 0;
for (const x = 100; 10, false; 100) {
for (const x = 100; false; 100) {
y = y + 1;
}
$[0] = y;
@@ -18,11 +18,10 @@ function foo() {
```javascript
function foo() {
const $ = React.useMemoCache();
const x = 100;
let y;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
y = 0;
while ((10, false)) {
while (false) {
y = y + 1;
}
$[0] = y;
@@ -27,18 +27,8 @@ function foo() {
```javascript
function foo() {
const a = 1;
const b = 2;
const c = 3;
const d = 3;
const e = 9;
const f = 3;
const g = -6;
console.log("foo");
const h = -6;
const i = -6;
const j = -6;
return j;
}
@@ -28,8 +28,6 @@ function Component(props) {
a = [];
b = {};
new Foo(a, b);
const _ = <div a={a}></div>;
new Foo(b);
$[0] = a;
$[1] = b;
@@ -33,7 +33,6 @@ function foo(a, b) {
} else {
x = $[1];
}
<div>{x}</div>;
const y = [];
if (x.length) {
@@ -19,7 +19,7 @@ function f(reader) {
## Error
```
[ReactForget] Todo: (CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block, tried to declare 'value$0'
[ReactForget] Todo: (CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block, tried to declare 'value'
```
@@ -32,10 +32,9 @@ function foo(props) {
for (
let x = 0;
x > props.min && x < props.max;
x = x$0 + (props.cond ? props.increment : (2, 2)), x
x = x$0 + (props.cond ? props.increment : 2), x
) {
const x$0 = x * 2;
const y$1 = y + x$0;
}
return y;
}
@@ -29,36 +29,35 @@ function Component(props) {
const $ = React.useMemoCache();
const cond = props.cond;
const x = props.x;
const a = undefined;
const c_0 = $[0] !== cond;
const c_1 = $[1] !== x;
let a$0;
let a;
if (c_0 || c_1) {
a$0 = undefined;
a = undefined;
if (cond) {
const a$1 = x;
a$0 = a$1;
const a$0 = x;
a = a$0;
} else {
let a$2;
let a$1;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
a$2 = [];
$[3] = a$2;
a$1 = [];
$[3] = a$1;
} else {
a$2 = $[3];
a$1 = $[3];
}
a$0 = a$2;
a = a$1;
}
$[0] = cond;
$[1] = x;
$[2] = a$0;
$[2] = a;
} else {
a$0 = $[2];
a = $[2];
}
useFreeze(a$0);
useFreeze(a$0);
call(a$0);
return a$0;
useFreeze(a);
useFreeze(a);
call(a);
return a;
}
function useFreeze(x) {}
@@ -15,19 +15,8 @@ function Component(c) {
```javascript
function Component(c) {
const $ = React.useMemoCache();
const c_0 = $[0] !== c;
let x;
if (c_0) {
x = { c: c };
mutate(x);
$[0] = c;
$[1] = x;
} else {
x = $[1];
}
const a = x;
const b = a;
const x = { c: c };
mutate(x);
}
```
@@ -41,10 +41,6 @@ function foo(a, b, c) {
} else {
y = $[3];
}
<div>
{x}
{y}
</div>;
const c_4 = $[4] !== x;
const c_5 = $[5] !== y.method;
const c_6 = $[6] !== b;
@@ -29,7 +29,6 @@ function foo(a, b, c) {
} else {
x = $[1];
}
<div>{x}</div>;
const method = x.method;
const c_2 = $[2] !== method;
@@ -28,7 +28,6 @@ function foo(a, b, c) {
} else {
x = $[1];
}
<div>{x}</div>;
const c_2 = $[2] !== x;
const c_3 = $[3] !== b;
let y;
@@ -14,8 +14,6 @@ function foo(a, b, c) {
```javascript
function foo(a, b, c) {
a[b] = c[b];
1;
2;
a[3] = c[b * 4];
}
@@ -16,12 +16,10 @@ function foo(a, b, c) {
```javascript
function foo(a, b, c) {
const x = a.x;
const y = b.c.d;
y.z = c.d.e;
foo(a.b.c);
[a.b.c];
}
```
@@ -55,7 +55,6 @@ function Component(props) {
x$0 = $[3];
}
const _ = <Component x={x$0}></Component>;
y.push(props.p2);
const c_5 = $[5] !== x$0;
const c_6 = $[6] !== y;
@@ -36,7 +36,6 @@ function Component(props) {
} else {
x$0 = $[3];
}
const _ = <Component x={x$0}></Component>;
y.push(props.p1);
$[0] = props.p0;
@@ -37,7 +37,6 @@ function Component(props) {
```javascript
function Component(props) {
const $ = React.useMemoCache();
const x = undefined;
const c_0 = $[0] !== props.cond;
const c_1 = $[1] !== props.test;
const c_2 = $[2] !== props.v0;
@@ -46,35 +45,35 @@ function Component(props) {
const c_5 = $[5] !== props.cond2;
const c_6 = $[6] !== props.b;
const c_7 = $[7] !== props.c;
let x$0;
let x;
if (c_0 || c_1 || c_2 || c_3 || c_4 || c_5 || c_6 || c_7) {
x$0 = undefined;
x = undefined;
bb1: if (props.cond) {
switch (props.test) {
case 0: {
const x$1 = props.v0;
x$0 = x$1;
const x$0 = props.v0;
x = x$0;
break bb1;
}
case 1: {
const x$2 = props.v1;
x$0 = x$2;
const x$1 = props.v1;
x = x$1;
break bb1;
}
case 2: {
}
default: {
const x$3 = props.v2;
x$0 = x$3;
const x$2 = props.v2;
x = x$2;
}
}
} else {
if (props.cond2) {
const x$4 = props.b;
x$0 = x$4;
const x$3 = props.b;
x = x$3;
} else {
const x$5 = props.c;
x$0 = x$5;
const x$4 = props.c;
x = x$4;
}
}
$[0] = props.cond;
@@ -85,11 +84,11 @@ function Component(props) {
$[5] = props.cond2;
$[6] = props.b;
$[7] = props.c;
$[8] = x$0;
$[8] = x;
} else {
x$0 = $[8];
x = $[8];
}
x$0;
x;
}
```
@@ -19,7 +19,6 @@ function foo() {}
```javascript
function sequence(props) {
const $ = React.useMemoCache();
null;
Math.max(1, 2);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
@@ -33,7 +32,6 @@ function sequence(props) {
x = t0;
while ((foo(), true)) {
foo();
2;
x = 2;
}
$[1] = x;
@@ -22,20 +22,19 @@ function foo() {
function mutate() {}
function foo() {
const $ = React.useMemoCache();
const a = {};
let c$2;
let c$1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const b = {};
const c = {};
const a$0 = b;
const b$1 = c;
c$2 = a$0;
mutate(a$0, b$1);
$[0] = c$2;
const a = b;
const b$0 = c;
c$1 = a;
mutate(a, b$0);
$[0] = c$1;
} else {
c$2 = $[0];
c$1 = $[0];
}
return c$2;
return c$1;
}
```
@@ -28,7 +28,7 @@ function Component(props) {
a = [];
b = {};
foo(a, b);
const _ = <div a={a}></div>;
foo(a, b);
$[0] = a;
$[1] = b;
@@ -20,7 +20,7 @@ function foo() {
let x;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
x = 1;
for (const i = 0; 10, true; 0) {
for (const i = 0; true; 0) {
x = x + 1;
}
$[0] = x;
@@ -28,13 +28,8 @@ function foo(a, b, c, d) {
```javascript
function foo(a, b, c, d) {
const x = 0;
true;
true;
const x$0 = a;
x$0;
return x$0;
const x = a;
return x;
}
```
@@ -24,14 +24,11 @@ function foo() {
function foo() {
const $ = React.useMemoCache();
const x = 1;
const y = 2;
1;
false;
const y$0 = 3;
const y = 3;
let t;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t = { x: x, y: y$0 };
t = { x: x, y: y };
$[0] = t;
} else {
t = $[0];
@@ -16,11 +16,8 @@ function foo(a, b, c) {
```javascript
function foo(a, b, c) {
const x = 0;
const x$0 = a;
const x$1 = b;
const x$2 = c;
return x$2;
const x = c;
return x;
}
```
@@ -17,12 +17,8 @@ function foo() {
```javascript
function foo() {
const x = 1;
1;
true;
const x$0 = 2;
return x$0;
const x = 2;
return x;
}
```
@@ -16,12 +16,8 @@ function foo() {
```javascript
function foo() {
const x = 1;
1;
true;
const x$0 = 2;
throw x$0;
const x = 2;
throw x;
}
```
@@ -18,7 +18,7 @@ function foo() {
```javascript
function foo() {
const x = 1;
while ((10, true)) {
while (true) {
1;
2;
}
@@ -36,30 +36,29 @@ function foo(x) {
```javascript
function foo(x) {
const y = undefined;
bb1: switch (x) {
case 0: {
const y$0 = 0;
const y = 0;
}
case 1: {
const y$1 = 1;
const y = 1;
break bb1;
}
case 2: {
break bb1;
}
case 3: {
const y$2 = 3;
const y = 3;
break bb1;
}
case 4: {
const y$3 = 4;
const y = 4;
}
case 5: {
const y$4 = 5;
const y = 5;
}
default: {
const y$5 = 0;
const y = 0;
}
}
}
@@ -41,11 +41,10 @@ function Component(props) {
case true: {
x.push(props.p2);
x.push(props.p3);
const y$1 = [];
}
case false: {
const y$2 = x;
y$0 = y$2;
const y$1 = x;
y$0 = y$1;
}
}
$[0] = props.p0;
@@ -39,7 +39,7 @@ function ternary(props) {
const c_5 = $[5] !== b;
let t2;
if (c_4 || c_5) {
t2 = a ? b : (null, null);
t2 = a ? b : null;
$[4] = a;
$[5] = b;
$[6] = t2;
@@ -16,23 +16,15 @@ function component() {
```javascript
function component() {
const $ = React.useMemoCache();
let x;
const x = {};
let q;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
x = {};
let q;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
q = {};
$[1] = q;
} else {
q = $[1];
}
x.t = q;
$[0] = x;
q = {};
$[0] = q;
} else {
x = $[0];
q = $[0];
}
const z = x.t;
x.t = q;
}
```
@@ -30,7 +30,6 @@ function component() {
} else {
p = $[0];
}
p + p;
let o;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
o = {};
@@ -38,19 +37,10 @@ function component() {
} else {
o = $[1];
}
let x;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
x = {};
x.t = p;
const z = x.t;
x.t = o;
$[2] = x;
} else {
x = $[2];
}
const y = x.t;
const x = {};
x.t = p;
x.t = o;
}
```
@@ -17,10 +17,8 @@ function foo(a) {
```javascript
function foo(a) {
const x = 0;
const x$0 = 1;
return a + x$0;
const x = 1;
return a + x;
}
```