@@ -13,16 +13,19 @@ import {
13
13
HIRFunction ,
14
14
IdentifierId ,
15
15
Instruction ,
16
+ InstructionId ,
17
+ Place ,
16
18
isExpressionBlockKind ,
19
+ makeInstructionId ,
17
20
markInstructionIds ,
18
21
} from "../HIR" ;
19
22
import { printInstruction } from "../HIR/PrintHIR" ;
20
23
import {
24
+ eachInstructionLValue ,
21
25
eachInstructionValueLValue ,
22
26
eachInstructionValueOperand ,
23
27
eachTerminalOperand ,
24
28
} from "../HIR/visitors" ;
25
- import { mayAllocate } from "../ReactiveScopes/InferReactiveScopeVariables" ;
26
29
import { getOrInsertWith } from "../Utils/utils" ;
27
30
28
31
/**
@@ -69,8 +72,9 @@ import { getOrInsertWith } from "../Utils/utils";
69
72
export function instructionReordering ( fn : HIRFunction ) : void {
70
73
// Shared nodes are emitted when they are first used
71
74
const shared : Nodes = new Map ( ) ;
75
+ const references = findReferencedRangeOfTemporaries ( fn ) ;
72
76
for ( const [ , block ] of fn . body . blocks ) {
73
- reorderBlock ( fn . env , block , shared ) ;
77
+ reorderBlock ( fn . env , block , shared , references ) ;
74
78
}
75
79
CompilerError . invariant ( shared . size === 0 , {
76
80
reason : `InstructionReordering: expected all reorderable nodes to have been emitted` ,
@@ -88,35 +92,103 @@ type Nodes = Map<IdentifierId, Node>;
88
92
type Node = {
89
93
instruction : Instruction | null ;
90
94
dependencies : Set < IdentifierId > ;
95
+ reorderability : Reorderability ;
91
96
depth : number | null ;
92
97
} ;
93
98
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
+
94
163
function reorderBlock (
95
164
env : Environment ,
96
165
block : BasicBlock ,
97
- shared : Nodes
166
+ shared : Nodes ,
167
+ references : References
98
168
) : void {
99
169
const locals : Nodes = new Map ( ) ;
100
170
const named : Map < string , IdentifierId > = new Map ( ) ;
101
171
let previous : IdentifierId | null = null ;
102
172
for ( const instr of block . instructions ) {
103
173
const { lvalue, value } = instr ;
104
174
// Get or create a node for this lvalue
175
+ const reorderability = getReorderability ( instr , references ) ;
105
176
const node = getOrInsertWith (
106
177
locals ,
107
178
lvalue . identifier . id ,
108
179
( ) =>
109
180
( {
110
181
instruction : instr ,
111
182
dependencies : new Set ( ) ,
183
+ reorderability,
112
184
depth : null ,
113
185
} ) as Node
114
186
) ;
115
187
/**
116
188
* Ensure non-reoderable instructions have their order retained by
117
189
* adding explicit dependencies to the previous such instruction.
118
190
*/
119
- if ( getReoderability ( instr ) === Reorderability . Nonreorderable ) {
191
+ if ( reorderability === Reorderability . Nonreorderable ) {
120
192
if ( previous !== null ) {
121
193
node . dependencies . add ( previous ) ;
122
194
}
@@ -172,66 +244,125 @@ function reorderBlock(
172
244
173
245
DEBUG && console . log ( `bb${ block . id } ` ) ;
174
246
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.
186
253
*/
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 (
191
278
env ,
192
279
locals ,
193
280
shared ,
194
- seen ,
281
+ nextInstructions ,
195
282
block . instructions . at ( - 1 ) ! . lvalue . identifier . id
196
283
) ;
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 ;
220
284
}
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 ;
231
300
}
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
+ }
235
366
}
236
367
237
368
block . instructions = nextInstructions ;
@@ -247,8 +378,7 @@ function getDepth(env: Environment, nodes: Nodes, id: IdentifierId): number {
247
378
return node . depth ;
248
379
}
249
380
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 ;
252
382
for ( const dep of node . dependencies ) {
253
383
depth += getDepth ( env , nodes , dep ) ;
254
384
}
@@ -265,7 +395,7 @@ function print(
265
395
depth : number = 0
266
396
) : void {
267
397
if ( seen . has ( id ) ) {
268
- console . log ( `${ "| " . repeat ( depth ) } $${ id } <skipped>` ) ;
398
+ DEBUG && console . log ( `${ "| " . repeat ( depth ) } $${ id } <skipped>` ) ;
269
399
return ;
270
400
}
271
401
seen . add ( id ) ;
@@ -282,11 +412,12 @@ function print(
282
412
for ( const dep of deps ) {
283
413
print ( env , locals , shared , seen , dep , depth + 1 ) ;
284
414
}
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
+ ) ;
290
421
}
291
422
292
423
function printNode ( node : Node ) : string {
@@ -336,7 +467,10 @@ enum Reorderability {
336
467
Reorderable ,
337
468
Nonreorderable ,
338
469
}
339
- function getReoderability ( instr : Instruction ) : Reorderability {
470
+ function getReorderability (
471
+ instr : Instruction ,
472
+ references : References
473
+ ) : Reorderability {
340
474
switch ( instr . value . kind ) {
341
475
case "JsxExpression" :
342
476
case "JsxFragment" :
@@ -348,6 +482,20 @@ function getReoderability(instr: Instruction): Reorderability {
348
482
case "UnaryExpression" : {
349
483
return Reorderability . Reorderable ;
350
484
}
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
+ }
351
499
default : {
352
500
return Reorderability . Nonreorderable ;
353
501
}
0 commit comments