























Study with the several resources on Docsity
Earn points by helping other students or get them with a premium plan
Prepare for your exams
Study with the several resources on Docsity
Earn points to download
Earn points by helping other students or get them with a premium plan
Community
Ask the community for help and clear up your study doubts
Discover the best universities in your country according to Docsity users
Free resources
Download our free guides on studying techniques, anxiety management strategies, and thesis advice from Docsity tutors
Compiler optimizations to improve target code, including machine-independent optimizations, control flow analysis, common sub-expression elimination, copy propagation, loop optimizations, basic block optimizations, and global dataflow analysis. Insights into how compilers collect and utilize program information for effective code optimization and generation. Challenges posed by control flow constructs and interval-based methods to handle them. Use of directed acyclic graphs for implementing transformations on basic blocks and an algorithm for rearranging instructions within a basic block to generate optimal code.
Typology: Study notes
1 / 31
This page cannot be seen from the preview
Don't miss anything!
The code produced by the straight forward compiling algorithms can often be made to run faster or take less space, or both. This improvement is achieved by program transformations that are traditionally called optimizations. Compilers that apply code-improving transformations are called optimizing compilers. Optimizations are classified into two categories. They are Machine independent optimizations: Machine dependant optimizations: Machine independent optimizations: Machine independent optimizations are program transformations that improve the target code without taking into consideration any properties of the target machine. Machine dependant optimizations: Machine dependant optimizations are based on register allocation and utilization of special machine-instruction sequences. The criteria for code improvement transformations: Simply stated, the best program transformations are those that yield the most benefit for the least effort. The transformation must preserve the meaning of programs. That is, the optimization must not change the output produced by a program for a given input, or cause an error such as division by zero, that was not present in the original source program. At all times we take the “safe” approach of missing an opportunity to apply a transformation rather than risk changing what the program does. A transformation must, on the average, speed up programs by a measurable amount. We are also interested in reducing the size of the compiled code although the size of the code has less importance than it once had. Not every transformation succeeds in improving every program, occasionally an “optimization” may slow down a program slightly. The transformation must be worth the effort. It does not make sense for a compiler writer to expend the intellectual effort to implement a code improving transformation and to have the compiler expend the additional time compiling source programs if this effort is not repaid when the target programs are executed. “Peephole” transformations of this kind are simple enough and beneficial enough to be included in any compiler.
Organization for an Optimizing Compiler: Flow analysis is a fundamental prerequisite for many important types of code improvement. Generally control flow analysis precedes data flow analysis. Control flow analysis (CFA) represents flow of control usually in form of graphs, CFA constructs such as control flow graph Call graph Data flow analysis (DFA) is the process of ascerting and collecting information prior to program execution about the possible modification, preservation, and use of certain entities (such as values or attributes of variables) in a computer program. PRINCIPAL SOURCES OF OPTIMISATION A transformation of a program is called local if it can be performed by looking only at the statements in a basic block; otherwise, it is called global. Many transformations can be performed at both the local and global levels. Local transformations are usually performed first. Function-Preserving Transformations There are a number of ways in which a compiler can improve a program without changing the function it computes. The transformations Common sub expression elimination, Copy propagation, Dead-code elimination, and Constant folding are common examples of such function-preserving transformations. The other transformations come up primarily when global optimizations are performed.
values that never get used. While the programmer is unlikely to introduce any dead code intentionally, it may appear as the result of previous transformations. An optimization can be done by eliminating dead code. Example: i=0; if(i=1) { a=b+5; } Here, „if‟ statement is dead code because this condition will never get satisfied. Constant folding : We can eliminate both the test and printing from the object code. More generally, deducing at compile time that the value of an expression is a constant and using the constant instead is known as constant folding. One advantage of copy propagation is that it often turns the copy statement into dead code. For example, a=3.14157/2 can be replaced by a=1.570 there by eliminating a division operation. Loop Optimizations: We now give a brief introduction to a very important place for optimizations, namely loops, especially the inner loops where programs tend to spend the bulk of their time. The running time of a program may be improved if we decrease the number of instructions in an inner loop, even if we increase the amount of code outside that loop. Three techniques are important for loop optimization: code motion, which moves code outside a loop; Induction-variable elimination, which we apply to replace variables from inner loop. Reduction in strength, which replaces and expensive operation by a cheaper one, such as a multiplication by an addition. Code Motion: An important modification that decreases the amount of code in a loop is code motion. This transformation takes an expression that yields the same result independent of the number of times a loop is executed ( a loop-invariant computation) and places the expression before the loop. Note that the notion “before the loop” assumes the existence of an entry for the loop. For example, evaluation of limit-2 is a loop-invariant computation in the following while-statement: while (i <= limit-2) /* statement does not change limit*/ Code motion will result in the equivalent of
t= limit-2; while (i<=t) /* statement does not change limit or t / Induction Variables : Loops are usually processed inside out. For example consider the loop around B3. Note that the values of j and t 4 remain in lock-step; every time the value of j decreases by 1, that of t 4 decreases by 4 because 4j is assigned to t 4. Such identifiers are called induction variables. When there are two or more induction variables in a loop, it may be possible to get rid of all but one, by the process of induction-variable elimination. For the inner loop around B3 in Fig. we cannot get rid of either j or t 4 completely; t 4 is used in B3 and j in B4. However, we can illustrate reduction in strength and illustrate a part of the process of induction-variable elimination. Eventually j will be eliminated when the outer loop of B
c: = a d: = b
Dead code elimination: It‟s possible that a large amount of dead (useless) code may exist in the program. This might be especially caused when introducing variables and procedures as part of constructio n or error-correction of a program – once declared and defined, one forgets to remove them in case they serve no purpose. Eliminating these will definitely optimize the code. Renaming of temporary variables: A statement t:=b+c where t is a temporary name can be changed to u:=b+c where u is another temporary name, and change all uses of t to u. In this we can transform a basic block to its equivalent block called normal-form block. Interchange of two independent adjacent statements: Two statements t 1 :=b+c t 2 :=x+y can be interchanged or reordered in its computation in the basic block when value of t (^1) does not affect the value of t 2. Algebraic Transformations: Algebraic identities represent another important class of optimizations on basic blocks. This includes simplifying expressions or replacing expensive operation by cheaper ones i.e. reduction in strength. Another class of related optimizations is constant folding. Here we evaluate constant expressions at compile time and replace the constant expressions by their values. Thus the expression 23.14 would be replaced by 6.28. The relational operators <=, >=, <, >, + and = sometimes generate unexpected common sub expressions. Associative laws may also be applied to expose common sub expressions. For example, if the source code has the assignments a :=b+c e :=c+d+b the following intermediate code may be generated: a :=b+c t :=c+d e :=t+b Example: x:=x+0 can be removed x:=y2 can be replaced by a cheaper statement x:=yy
If ( debug ) { Print debugging information } In the intermediate representations the if-statement may be translated as: If debug =1 goto L2 goto L L1: print debugging information L2:.........................................................................................(a) One obvious peephole optimization is to eliminate jumps over jumps .Thus no matter what the value of debug ; (a) can be replaced by: If debug ≠1 goto L Print debugging information L2:...........................................................................................(b) As the argument of the statement of (b) evaluates to a constant true it can be replaced by If debug ≠0 goto L Print debugging information L2:..........................................................................................(c) As the argument of the first statement of (c) evaluates to a constant true, it can be replaced by goto L2. Then all the statement that print debugging aids are manifestly unreachable and can be eliminated one at a time. Flows-Of-Control Optimizations: The unnecessary jumps can be eliminated in either the intermediate code or th e target code by the following types of peephole optimizations. We can replace the jump sequence goto L …. L1: gotoL by the sequence
goto L 2 …. L1: goto L If there are now no jumps to L1, then it may be possible to eliminate the statement L1:goto L2 provided it is preceded by an unconditional jump .Similarly, the sequence if a < b goto L …. L1: goto L can be replaced by If a < b goto L …. L1: goto L Finally, suppose there is only one jump to L1 and L1 is preceded by an unconditional goto. Then the sequence goto L L1: if a < b goto L L3:..........................................................................(1) May be replaced by If a < b goto L goto L ……. L3:............................................................................(2) While the number of instructions in (1) and (2) is the same, we sometimes skip the unconditional jump in (2), but never in (1).Thus (2) is superior to (1) in execution time Algebraic Simplification: There is no end to the amount of algebraic simplification that can be attempted through
This equation can be read as “ the information at the end of a statement is either generated within the statement , or enters at the beginning and is not killed as control flows through the statement.” The details of how data-flow equations are set and solved depend on three factors. The notions of generating and killing depend on the desired information, i.e., on the data flow analysis problem to be solved. Moreover, for some problems, instead of proceeding along with flow of control and defining out[s] in terms of in[s], we need to proceed backwards and define in[s] in terms of out[s]. Since data flows along control paths, data-flow analysis is affected by the constructs in a program. In fact, when we write out[s] we implicitly assume that there is unique end point where control leaves the statement; in general, equations are set up at the level of basic blocks rather than statements, because blocks do have unique end points. There are subtleties that go along with such statements as procedure calls, assignments through pointer variables, and even assignments to array variables. Points and Paths: Within a basic block, we talk of the point between two adjacent statements, as well as the point before the first statement and after the last. Thus, block B1 has four points: one before any of the assignments and one after each of the three assignments. Now let us take a global view and consider all the points in all the blocks. A path from p (^1) to pn is a sequence of points p 1 , p 2 ,….,pn such that for each i between 1 and n-1, either
Pi is the point immediately preceding a statement and pi+1 is the point immediately following that statement in the same block, or Pi is the end of some block and pi+1 is the beginning of a successor block. Reaching definitions: A definition of variable x is a statement that assigns, or may assign, a value to x. The most common forms of definition are assignments to x and statements that read a value from an i/o device and store it in x. These statements certainly define a value for x, and they are referred to as unambiguous definitions of x. There are certain kinds of statements that may define a value for x; they are called ambiguous definitions. The most usual forms of ambiguous definitions of x are: A call of a procedure with x as a parameter or a procedure that can access x because x is in the scope of the procedure. An assignment through a pointer that could refer to x. For example, the assignment * q: = y is a definition of x if it is possible that q points to x. we must assume that an assignment through a pointer is a definition of every variable. We say a definition d reaches a point p if there is a path from the point immediately following d to p, such that d is not “killed” along that path. Thus a point can be reached by an unambiguous definition and an ambiguous definition of the same variable appearing later along one path. Data-flow analysis of structured programs: Flow graphs for control flow constructs such as do-while statements have a useful property: there is a single beginning point at which control enters and a single end point that control leaves from when execution of the statement is over. We exploit this property when we talk of the definitions reaching the beginning and the end of statements with the following syntax.
a statement‟s region are the beginning and end points, respectively, of the statement. The equations are inductive, or syntax-directed, definition of the sets in[S], out[S], gen[S], and kill[S] for all statements S. gen[S] is the set of definitions “generated” by S while kill[S] is the set of definitions that never reach the end of S. Consider the following data-flow equations for reaching definitions : i ) S d^ :^ a^ :^ =^ b^ +^ c gen [S] = { d } kill [S] = Da – { d } out [S] = gen [S] U ( in[S] – kill[S] ) Observe the rules for a single assignment of variable a. Surely that assignment is a definition of a, say d. Thus Gen[S]={d} On the other hand, d “kills” all other definitions of a, so we write Kill[S] = Da – {d} Where, Da is the set of all definitions in the program for variable a. ii ) S S 1 S 2 gen[S]=gen[S 2 ] U (gen[S 1 ]-kill[S 2 ]) Kill[S] = kill[S 2 ] U (kill[S 1 ] – gen[S 2 ])
in [S 1 ] = in [S] in [S 2 ] = out [S 1 ] out [S] = out [S 2 ]
depending on in. we intend that in[S] be the set of definitions reaching the beginning of problem. It turns out out is a synthesized attribute
S, taking into account the flow of control throughout the entire program, including statements outside of S or within which S is nested. The set out[S] is defined similarly for the end of s. it is important to note the distinction between out[S] and gen[S]. The latter is the set of definitions that reach the end of S without following paths outside S. Assuming we know in[S] we compute out by equation, that is Out[S] = gen[S] U (in[S] - kill[S]) Considering cascade of two statements S 1 ; S 2 , as in the second case. We start by observing in[S 1 ]=in[S]. Then, we recursively compute out[S 1 ], which gives us in[S 2 ], since a definition reaches the beginning of S 2 if and only if it reaches the end of S 1. Now we can compute out[S 2 ], and this set is equal to out[S]. Considering if-statement we have conservatively assumed that control can follow either branch, a definition reaches the beginning of S 1 or S 2 exactly when it reaches the beginning of S. In[S 1 ] = in[S 2 ] = in[S] If a definition reaches the end of S if and only if it reaches the end of one or both sub statements; i.e, Out[S]=out[S 1 ] U out[S 2 ] Representation of sets: Sets of definitions, such as gen[S] and kill[S], can be represented compactly using bit vectors. We assign a number to each definition of interest in the flow graph. Then bit vector representing a set of definitions will have 1 in position I if and only if the definition numbered I is in the set. The number of definition statement can be taken as the index of statement in an array holding pointers to statements. However, not all definitions may be of interest during global data-flow analysis. Therefore the number of definitions of interest will typically be recorded in a separate table. A bit vector representation for sets also allows set operations to be implemented efficiently. The union and intersection of two sets can be implemented by logical or and logical and, respectively, basic operations in most systems-oriented programming languages. The difference A-B of sets A and B can be implemented by taking the complement of B and then using logical and to compute A