summaryrefslogtreecommitdiffstats
path: root/source/slang/ir-dce.cpp
blob: 0f037bfe5e2e99df6f7144dfc40823389a845775 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
// ir-dce.cpp
#include "ir-dce.h"

#include "ir.h"
#include "ir-insts.h"

namespace Slang
{

struct DeadCodeEliminationContext
{
    // This type implements a simple global DCE pass over
    // an entire module.
    //
    // We start with member variables to stand in for
    // the parameters that were passed to the top-level
    // `eliminateDeadCode` function.
    //
    CompileRequest* compileRequest;
    IRModule*       module;

    // Our overall process is going to be to determine
    // which instructions in the module are "live"
    // and then eliminate anything that wasn't found to
    // be live.
    //
    // We will track the liveness state by keeping
    // a set of all instructions we have so far determined
    // to be live.
    //
    HashSet<IRInst*> liveInsts;

    // Querying whether an instruction has been
    // determined to be live is easy.
    //
    bool isInstLive(IRInst* inst)
    {
        // The only wrinkle is that we want to safeguard
        // against a null instruction (there are some
        // corner cases where we still construct IR
        // instructions with a null type).
        //
        if(!inst) return false;

        return liveInsts.Contains(inst);
    }

    // We are going to do an iterative analysis
    // where we mark instructions we know are
    // live, and then see if that can help us
    // identify any other instructions that
    // must also be live.
    //
    // For this, we will use a work list of
    // instructions that have been marked
    // as live, but for which we haven't
    // looked at their impact on other
    // instructions.
    //
    List<IRInst*> workList;

    // When we discover that an instruction seems
    // to be live, we will add it to our set,
    // and also the work list, but only if we
    // haven't done so previously.
    //
    void markInstAsLive(IRInst* inst)
    {
        // Again, we safeguard against null instructions
        // just in case.
        //
        if(!inst) return;

        if(liveInsts.Contains(inst))
            return;
        liveInsts.Add(inst);
        workList.Add(inst);
    }

    // Given the basic infrastructrure above, let's
    // dive into the task of actually finding all
    // the live code in a module.
    //
    void processModule()
    {
        // First of all, we know that the root module instruction
        // should be considered as live, because otherwise
        // we'd end up eliminating it, so that is a
        // good place to start.
        //
        markInstAsLive(module->getModuleInst());

        // Marking the module as live should have
        // seeded our work list, so we can now start
        // processing entries off of our work list
        // until it goes dry.
        //
        while( workList.Count() )
        {
            auto inst = workList.Last();
            workList.RemoveLast();

            // At this point we know that `inst` is live,
            // and we want to start considering which other
            // instructions must be live because of that
            // knowlege.
            //
            // A first easy case is that the parent (if any)
            // of a live instruction had better be live, or
            // else we might delete the parent, and
            // the child with it.
            //
            markInstAsLive(inst->getParent());

            // Next the type of a live instruction, and all
            // of its operands must also be live, or else
            // we won't be able to compute its value.
            //
            markInstAsLive(inst->getFullType());
            UInt operandCount = inst->getOperandCount();
            for( UInt ii = 0; ii < operandCount; ++ii )
            {
                markInstAsLive(inst->getOperand(ii));
            }

            // Finally, we need to consider the children
            // and decorations of the instruction.
            //
            // Note that just because an instruction is
            // live doesn't mean its children must be, or
            // else we'd never eliminate *anything* (we
            // marked the whole module as live, and everything
            // is a transitive child of the module).
            //
            // Decorations, in contrast, are always live if their
            // parents are (because we don't want to silently drop
            // decorations). It is still important to *mark*
            // decorations as live, because they have operands,
            // and those operands need to be marked as live.
            // We will fold decorations into the same loop
            // as children for simplicity.
            //
            // To keep the code here simple, we'll defer the
            // decision of whether a child (or decoration)
            // should be live when its parent is to a subroutine.
            //
            for( auto child : inst->getDecorationsAndChildren() )
            {
                if(shouldInstBeLiveIfParentIsLive(child))
                {
                    // In this case, we know `inst` is live and
                    // its `child` should be live if its parent is,
                    // so the `child` must be live too.
                    //
                    markInstAsLive(child);
                }
            }
        }

        // If our work list runs dry, that means we've reached a steady
        // state where everything that is transitively relevant to
        // the "outputs" of the module has been marked as live.
        //
        // Now we can simply walk through all of our instructions
        // recursively and eliminate those that are "dead" by
        // virtue of not having been found live.
        //
        eliminateDeadInstsRec(module->getModuleInst());
    }

    void eliminateDeadInstsRec(IRInst* inst)
    {
        // Given the instruction `inst` we need to eliminate
        // any dead code at, or under it.
        //
        // The easy case is if `inst` is dead (that is, not live).
        //
        if( !isInstLive(inst) )
        {
            // We can simply remove and deallocate `inst` because it is
            // dead, and not worry about any of its descendents,
            // because they must have been dead too (since we always
            // mark the parent of a live instruction as live).
            //
            inst->removeAndDeallocate();
        }
        else
        {
            // If `inst` is live, then we need to deal with the possibility
            // that its children/decorations (or descendents in general)
            // might still be dead.
            //
            // The biggest wrinkle is that we walk the linked list of
            // children/decorations a bit carefully, using a temporary
            // to hold the next node, in case we eliminate one of
            // the children as we go.
            //
            IRInst* next = nullptr;
            for( IRInst* child = inst->getFirstDecorationOrChild(); child; child = next )
            {
                next = child->getNextInst();
                eliminateDeadInstsRec(child);
            }
        }
    }

    // Now we come to the decision procedure we put off before:
    // should a given `inst` be live if its parent is?
    //
    bool shouldInstBeLiveIfParentIsLive(IRInst* inst)
    {
        // The main source of confusion/complexity here is that
        // we are using the same routine to decide:
        //
        // * Should some ordinary instruction in a basic block be kept around?
        // * Should a basic block in some function be kept around?
        // * Should a function/type/variable in a module be kept around?
        //
        // Still, there are a few basic patterns we can observe.
        // First, if `inst` is an instruction that might have some effects
        // when it is executed, then we should keep it around.
        //
        if(inst->mightHaveSideEffects())
            return true;
        //
        // The `mightHaveSideEffects` query is conservative, and will
        // return `true` as its default mode, so once we are past that
        // query we know that `inst` is either something "structural"
        // (that makes up the program) rather than executable, or it
        // is executable but was on a white list of things that are
        // safe to eliminate.

        // Most top-level objects (functions, types, etc.) obviously
        // do *not* have side effects. That creates the risk that
        // we'll just go ahead and eliminate every single function/type
        // in a module. There needs to be a way to identify the
        // functions we want to keep around, and for right now
        // that is handled with the `[entryPoint]` decoration.
        //
        if(inst->findDecorationImpl(kIROp_EntryPointDecoration))
            return true;
        //
        // TODO: Eventually it would make sense to consider everything
        // with an `[export(...)]` decoration as live, but our current
        // approach to linking for back-end compilation leaves many
        // linkage decorations in place that we seemingly don't need/want.

        // A basic block is an interesting case. Knowing that a function
        // is live means that its entry block is live, but the liveness
        // of any other blocks is determined by whether they are referenced
        // by other instructions (e.g., a branch from one block to
        // another).
        //
        if( auto block = as<IRBlock>(inst) )
        {
            // To determine whether this is the first block in its
            // parent function (or what-have-you) we can simply
            // check if there is a previous block before it.
            //
            auto prevBlock = block->getPrevBlock();
            return prevBlock == nullptr;
        }

        // There are a few special cases of "structural" instructions
        // that we don't want to eliminate, so we'll check for those next.
        //
        switch( inst->op )
        {
            // Function parameters obviously shouldn't get eliminated,
            // even if nothing references them, and block parameters
            // (phi nodes) will be considered live when their block is,
            // just so that we don't have to deal with any complications
            // around re-writing the relevant inter-block argument passing.
            //
            // TODO: A smarter DCE pass could deal with this case more
            // carefully, or we could improve the interprocedural SCCP
            // pass to deal with block parameters instead.
            //
        case kIROp_Param:
            return true;

            // IR struct types and witness tables are currently kludged
            // so that they have child instructions that represent their
            // entries (effectively `(key,value)` pairs), and those child
            // instructions are never directly referenced (e.g., an access
            // to a struct field references the *key* but not the `(key,value)`
            // pair that is the `IRField` instruction.
            //
            // TODO: at some point the IR should use a different representation
            // for struct types and witness tables that does away with
            // this problem.
            //
        case kIROp_StructField:
        case kIROp_WitnessTableEntry:
            return true;

        default:
            break;
        }

        // If none of the explicit cases above matched, then we will consider
        // the instruction to not be live just because its parent is. Further
        // analysis could still lead to a change in the status of `inst`, if
        // an instruction that uses it as an operand is marked live.
        //
        return false;
    }
};

// The top-level function for invoking the DCE pass
// is straighforward. We set up the context object
// and then defer to it for the real work.
//
void eliminateDeadCode(
    CompileRequest* compileRequest,
    IRModule*       module)
{
    DeadCodeEliminationContext context;
    context.compileRequest = compileRequest;
    context.module = module;

    context.processModule();
}

}