Kompilatorer och interpretatorer: Lecture 13

Note: This is an outline of what I intend to say on the lecture. It is not a definition of the course content, and it does not replace the textbook.

Today: Optimization.
ASU chapter 10. (KP chapter 6.)

10. Code Optimization

Optimization: faster, smaller. (Not "optimal", just "better".)

"Hand optimization", example:

  for (i = 0; i < n; ++i) {
    do_something(a[i]);
  }
Equivalent to:
  i = 0;
  while (i < n) {
    do_something(a[i]);
    ++i;
  }
Can be "optimized" to:
  p = &a[0];
  p_after = &a[n];
  while (p != p_after) {
    do_something(*p);
    ++p;
  }
Probably no effect, or even slower. Better handled by the compiler!

Two rules about optimization by hand:

  1. Don't do it. (Usually not needed. If it is needed, leave it to the compiler.)
  2. Don't do it yet. ("90/10 rule". Profile first!)

Types of optimization:

  1. Algorithms and data structures. (Ex: Change sorting algorithm, or replace a linked list with a hash table.) Best gains (years to seconds)! Hard for the compiler. But: SQL!
  2. "Low-level" optimzation. (As above.) Better done by the compiler!
  3. Machine-dependent optimizations. (Register allocation, instruction choice, as in chapter 9. Instruction reordering to improve pipe-lining, etc.)

Peep-hole optimization (ASU 9.9): Simple transformations of the generated assembly (or machine) code. Ex:

MOV R0, a
MOV a, R0
can be changed to
MOV R0, a

10.1 Introduction

A quicksort function (adapted from ASU fig 10.2):
void quicksort(int m, int n) {
  int i, j;
  int v, x;
  if (n <= m)
    return;
  i = m-1; j = n; v = a[n];
  while (1) {
    do
      i = i + 1;
    while (v > a[i]);
    do
      j = j - 1;
    while (a[j] > v);
    if (i>=j)
      break;
  }
  x = a[i]; a[i] = a[n]; a[j] = x;
  quicksort(m, j);
  quicksort(i+1, n);
}
Some optimizations are not possible on the source level.
Example in Pascal: a[i]
Three-address code: t1 = 4*i; t2 = a[t1];
A Pascal compiler (and a C programmer!) can replace some of the 4*i calculations.

ASU fig. 10.4, three-address code for a part of the quicksort function:

30 three-address statements

Steps for the optimizer:

  1. Control flow analysis: basic blocks
  2. Data flow analysis.
  3. Transformations.

ASU fig. 10.5, basic blocks and flow graph for the quicksort function:

Six basic blocks in a flow graph

Three loops:

10.2 The principal sources of optimization

"Some of the most useful code-improving transformations".
Local transformation = inside a single basic block
Global transformation = several blocks (but inside a single procedure)

Function-preserving transformations (ASU p. 592)

From ASU fig. 10.6, eliminating common subexpressions inside a basic block:

B5:

t6 := 4*i
x := a[t6]
t7 := 4*i
t8 := 4*j
t9 := a[t8]
a[t7] := t9
t10 := 4*j
a[t10] := x
goto B2
can be changed to
t6 := 4*i
x := a[t6]
t8 := 4*j
t9 := a[t8]
a[t6] := t9
a[t8] := x
goto B2

(Remove repeat calculations, use t6 instead of t7, t8 instead of t10.)

Removing (non-local) common subexpressions (ASU p. 592-594)

Globally, an expression E is a common subexpression if E was previously computed, and the values of the variables in E haven't changed since then.

From fig 10.5/10.6:
(We can remove 4*i, 4*j, and 4*n completely from B6!)
Remove 4*i and 4*j completely from B5!

B5:

t6 := 4*i
x := a[t6]
t8 := 4*j
t9 := a[t8]
a[t6] := t9
a[t8] := x
goto B2
can be changed to
x := a[t2]
t9 := a[t4]
a[t2] := t9
a[t4] := x
goto B2

x := a[t2]
t9 := a[t4]
a[t2] := t9
a[t4] := x
goto B2
can then be changed to
x := t3
t9 := a[t4]
a[t2] := t9
a[t4] := x
goto B2

Note: a hasn't changed, so a[t4] still in t5 from B3

x := t3
t9 := a[t4]
a[t2] := t9
a[t4] := x
goto B2
can then be changed to
x := t3
a[t2] := t5
a[t4] := x
goto B2

ASU fig. 10.7, after eliminating (global) common subexpressions:

Basic blocks B5 and B6 have shrunk

Copy propagation (ASU p. 594-595)

Instead of:
copy = original; ... copy ...;
Always try to use:
copy = original; ... original ...;
(We may be able to eliminate the variable copy altogether!)

B5:

x := t3
a[t2] := t5
a[t4] := x
goto B2
can be changed to
x := t3
a[t2] := t5
a[t4] := t3
goto B2

Elimination of dead code (ASU p. 595)

Dead variables will never be used again.
Dead code (or useless code) computes values that will never be used,
Dead code can also mean code that can never be reached:
debug = 0; ... if (debug) printf(...);

B5:

x := t3
a[t2] := t5
a[t4] := t3
goto B2
but x is dead, so:
a[t2] := t5
a[t4] := t3
goto B2

Loop optimizations (ASU pp. 596-598)

Examples of code motion

while (i < limit - 2)
  a[i] = x + y;
The expressions limit - 2 and x + y are loop-invariant.
t1 = limit - 2;
t2 = x + y;
while (i < t)
  a[i] = t2;
The next example is harder for the compiler, since strlen is just another function. (Or is it?)
for (i = 0; i < strlen(s1); ++i)
  s2[i] = s1[i];
n = strlen(s1);
for (i = 0; i < n; ++i)
  s2[i] = s1[i];

Example of elimination of induction variables

i = 0;
j = 1;
while (a1[i] != 0) {
  a2[j] = a1[i];
  ++i;
  ++j;
}
Both i and j are induction variables ("loop counters").
i = 0;
while (a1[i] != 0) {
  a2[i + 1] = a1[i];
  ++i;
}

Example of both induction-variable elimination and reduction in strength

ASU fig 10.9, strength reduction of 4 * j in basic block B3.
We know that t4 == 4 * j. Maintain this relationship!

t4 - 4 instead of 4 * j

Then a similar strength reduction of 4 * i in basic block B2.
We know that t2 == 4 * i. Maintain this relationship!

Then, i and j are used only in the test int B4.
The test i >= j can be changed to 4 * t2 >= 4 * t4 (which is equivalent to t2 >= t4).
i and j become dead!

ASU fig 10.10, after eliminating induction variables i and j:

After eliminating i and j

Loop unrolling

for (i = 0; i < 20; ++i)
  for (j = 0; j < 2; ++j)
    a[i][j] = i + 2 * j;
may be transformed by unrolling the inner loop:
for (i = 0; i < 20; ++i) {
    a[i][0] = i;
    a[i][1] = i + 2;
}

10.3 - 10.12

Skip.

10.13 Symbolic debugging of optimized code

int a;
...
void f(string a) {
  ...
  while (1) {
    int a;
    ...        <-- In the debugger: print a
  }
}
What is needed, for symbolic debugging in general? Why debug optimized code? Why not debug unoptimized code, and only turn on the debugger when the program finally works?

A program may work when unoptimized, but not when optimized. For example, the C standard specifies undefined behaviour in certain cases. such as:

char s[10];
...
s[14] = 'x';
The behaviour depends entirely on what happens to be stored at the memory location 5 bytes after the end of s: unused padding, a variable, or the return address in an activation record? This can be different with or without optimization, since optimization, for example, can eliminate variables.

Deducing values of variables in basic blocks

When the user wants the debugger to show the current value of a: Other problems: A solution (not on the exam):
Let the compiler generate enough information for the debugger to deduce ("find out") the value of a variable. (Doesn't work always.)

ASU fig 10.68. Assume that the source, intermediate and target representation are the same.

Source and optimized code

ASU fig 10.69. A DAG for the variables. The DAG shows how values depend on each other. Then, annotate it with life-time information.

Annotated DAG

Example 1 (just the unoptimized program):
c = a + b after step 1. (Life time: 2-3)
c = c - e after step 3. (Life time: 4-infinity)

Example 2 (the optimized program):
c = a after step 5'. (Life time: 6'-infinty)
c is undefined in 1'-5'! (But can be calculated, differently depending on when!)

Example 3:
An overflow occurs in the optimized code, in statement 2', t = b * e.
The first source statement that uses the node b * e is 5.
Therefore, tell the user the program crashed in source statement 5!

Then the user says:

print b -> show b (lifetime 1-5 and 1'-4')
Explanation: b still has its initial value, both at 2' and at 5.

print c -> can't show actual stored c (lifetime 6-infinity)
Instead, find the DAG node for c at time 5 (-).
(Optimized) a will contain this value after 4', but not yet!
Consider the children: d contains the value from the + node at 2'-infinity. e contains the value from the E0 node at 1'-infinity. So use d - e!


Thomas Padron-McCarthy (Thomas.Padron-McCarthy@tech.oru.se) March 2, 2003