Procedure Optimizations and Interprocedural Analysis Chapter 15, 19 Mooly Sagiv
Outline Modularity Issues Procedure optimizations Interprocedural Optimizations Challenges The Call Graph Flow insensitive information Flow sensitive information Conclusions
Modularity Issues Procedures provide a mechanism for modularity Procedure bodies become smaller Machines becomes stronger Often procedures implement general algorithms How to achieve performance of a single procedure in a complex software? Two solutions : –procedure integration/inline/tail call elimination –interprocedural analysis
Procedure Optimizations Improve the code of a procedure –Reduce procedure overhead –Enables other intraprocedural optimizations Examples –Tail-call elimination and Tail-Recursion elimination –Procedure integration –In-Line Expansion –Leaf-Routine Optimization –Shrink Wrapping
Tail-Call and Tail-Recursive Elimination void make_node(p, n) struct node *p; int n; { struct node *q; q = malloc(sizeof(struct node)); q->next=nil; q->value=n; p->next=q; } void insert_node(n, l) int n; struct node *l; { if (n>l->value) if (l->next==nil) make_node(l, n); else insert_node(n, l->next); }
Procedure Integration Some programming languages allow user annotations (C++, ada) How to decide when to inline: –Single vs multiple compilation units Multiple programming languages –Intermediate level –Code improvement criteria Call cites in nested loops –Enables other optimizations Profiling Constant parameters –What about recursive procedures? –Code size –Cache effect and register pressure –Other compiler assumptions
Typical Heuristics for Procedure Integration The size of procedure body The number of calls to this procedure Is the procedure is called inside loop? Whether a call includes constant parameters? Use both static analysis and profiling (if available) Interprcedural analysis will lead to better results
Name Capture in Procedure integration g(b, c) int b, c; { int a, d; a = b + c; d= b * c; return d; } f() { int a, e; a = 2; e = g(3, 4); printf(“%d\n”, a); } f() { int a, e, d; a = 2; a = b + c; d = b * c; e = d; printf(“%d\n”, a); }
In-Line Expansion Substitute low level code (assembly level) Increase opportunity for using machine capabilities Poor man’s procedure integration Two essential mechanisms: –Define templates of machine sequences –A compiler inliner
Leaf-Routine Optimization Do not call other routines In practice half of the routines! Eliminate prolog and epilog
Shrink Wrapping Move prolog and epilog into the place where it is necessary or remove The general idea –Move prolog forward –Move epilog backward Requires data-flow analysis
Shrink Wrapping (Example) save r8-r15 restore r8-r15 r2 <= 10 r1 r1 + 1 r8 r2 – 1 r1 r8 + 2 r1 r1 + r2 Y N
Wrap-Up (Chapter 15) Whole procedure optimization Applied early Allows other optimization Most do not require data-flow analysis
Interprocedural Optimizations Can be used for procedure integration Constant propagation can be used to optimize procedure bodies Constant propagation can be used to “clone” procedures Call-by-value parameters can be passed by reference (if they don’t change) Register allocation Improve intraprocedural information
char * Red = “red”; char * Yellow = “yellow”; char * Orange = “orange”; char * color(FRUIT CurrentFruit); { switch (currentFruit->variety) { case APPLE: return Red; break; case BANANA: return Yellow; break; case ORANGE: return Orange; }} main() { FRUIT snack; snack.variety = APPLE; snack.shape = ROUND; printf(“%s\n”, color(&snack));} char * Red = “red”; char * Yellow = “yellow”; char * Orange = “orange”; main() { FRUIT snack; VARIETY t1; SHAPE t2; COLOR t3; t1 = APPLE; t2 = ROUND; switch (t1) { case APPLE: t3= Red; break; case BANANA: t3=Yellow; break; case ORANGE: t3=Orange; }} printf(“%s\n”, t3);}
Pascal Example with value parameters type vector = array[1…1000] of integer procedure p(v: vector); procedure q; var a: vector; p(a);
C Example For Constant Propagation int g; p() { … } q(){ … g=100; p(); y = g;
Challenges in Interprocedral Analysis Handling recursion Parameter passing mechanisms Hidden calls –Virtual methods –Function pointers –Procedural parameters –Higher order functions Scalability Supporting separate compilation mode
The Call Graph A finite directed multi-graph A node per procedure A labeled edge per call site Can be constructed incrementally to support separate compilation mode Difficult to construct in the presence of hidden calls
Example for Call Graph Construction 1: void f() { 2: g(); 3: g(); 4: h();} 5: void g() { 6: h(); 7: i(); } 8: void h() {} 9: void i() { 10: g() ;}
Obstacles Procedural parameters (P-SPACE hard) Higher order functions Virtual methods Solutions –Conservative approximation –Data-Flow information –Specialization
Flow insensitive side effect analysis Ignore control flow Compute for every call site: –MOD - the variables that may be modified –DEF - the variables must be defined –USE - the set of variables that may be used before set Can be computed efficiently for programs with small number of parameters (Cooper & Kennedy) Can be used for: –program understanding –replacing value by reference parameter –improving intraprocedural information Becomes tricky with nesting and aliases
program test; var a. b: integer; procedure g(var f1: integer) begin 1: f1 := f1 + 1; end procedure f(var f2, f3: integer) begin 2: g(f2); 3: f3 := f2 ; 4: g(f3); end begin (* main) 5: a := 5; 6: f(a, b); end.
program test; var a. b: integer; procedure g(var f1: integer) begin 1: f1 := f1 + 1; end procedure f(var f2, f3: integer) begin 2: g(f2); 3: f3 := f2 ; 4: g(f3); end begin (* main) 5: a := 5; 6: f(a, b); end.
Flow insensitive “points-to” analysis Analyze C pointers Find if a pointer “a” may-point to a pointer “b” Efficient conservative solutions exit a = &x; b = & y; if (…) y = & z; else y = &x; c = &y ; *c = t ;
Flow Sensitive Data-Flow Information Integrate control flow graph and call graph (the program supergraph) In the presence of reference parameters even bit vector problems are hard Two main solutions: –call strings –functional Scaling is an issue
Non trivial constants int x void p(int a) { int c; scanf(“%\d”, &c); if (c > 0) { a = a -2; p(a); a := a +2; } x := -2 * a + 5 printf(“%s\n”, x);} void main() { p(7); printf(“%s\n”, x) ; }
Conclusion Interprocedural analysis will be integrated into future compilers Can be implemented at link time –Register allocation Will lead to simpler programming Flow insensitive analysis scales But what about flow sensitive?