0% found this document useful (0 votes)
195 views20 pages

A Brief Odyssey of Dataflow Analysis in Optimizing Compilers

This document summarizes dataflow analysis, a technique for analyzing information flow in programs. It begins by introducing concepts like control flow graphs and basic blocks. It then motivates dataflow analysis by using a simple example to show how it can identify dead code assignments that are never used. The document proceeds to formally define concepts like live variables and uses an example to illustrate how dataflow analysis works by propagating live variable information backwards through the program. It shows initializing the analysis and applying constraints to iteratively determine the live variables at each program point.

Uploaded by

Lee Gao
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
195 views20 pages

A Brief Odyssey of Dataflow Analysis in Optimizing Compilers

This document summarizes dataflow analysis, a technique for analyzing information flow in programs. It begins by introducing concepts like control flow graphs and basic blocks. It then motivates dataflow analysis by using a simple example to show how it can identify dead code assignments that are never used. The document proceeds to formally define concepts like live variables and uses an example to illustrate how dataflow analysis works by propagating live variable information backwards through the program. It shows initializing the analysis and applying constraints to iteratively determine the live variables at each program point.

Uploaded by

Lee Gao
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 20

A Brief Odyssey of Dataow Analysis in Optimizing Compilers

Lee Gao November 11, 2012


Abstract The corner-stone of program code optimization is the ability to gather accurate and relevant information revolving around the control ow of the program. To this end, several static analysis methods can be applied. We present an old but powerful technique in analyzing information ow on graphs representing the control ow of our program, known as dataow analysis.

Introduction

Before we take o, we need to explain some of the concepts and terminology that will be used throughout this paper. First of all, we will represent programs and computations using an abstract assembly language extended with innite registers as well as innite memory. Suppose that our language consists of the following instructions at the procedure level (without consideration for how procedures themselves are structured, as we only consider the ow of data within procedures rather than as a whole program)
stmt := move(dest, e2 ) | exp(e) | seq(s1 , s2 ) | jump(l) | cjump(e, l) | return dest must be either a register or a mem(e) evaluates expression e for its side eects executes statement s1 and then s2 jump to address l jump to l if e is true returns from this procedure

where we consider programs and procedures as sequences of statements, which are themselves stmt. Here, we use e and s as symbolic variables representing expressions and statements respectively. Furthermore, we allow expressions to be of the form
expr := n | x | e1 e2 | e | mem e | lfunction (e1 , , en )
n

numerical constants register x, represented as a variable binary operations unary negation memory address calls the function at label lfunction

The notation for registers (x) and memory addresses (mem e) may seem a little mysterious. In short, if its used as the destination of a move instruction, it will be used as a storage 1

container. (e.g. move(x, 1) is equivalent to x := 1; move(mem(A + 0), 0) is equivalent to A[0] := 0). Furthermore, we reserve a special register R which will contain the return values from functions. In this notation, we can represent programs as a large nested seq statement; for example, we can write a program that sums an array: Listing 1 Sum over the elements of a 1: function Sum(a, n) 2: move(i, 0); 3: lstart loop : 4: cjump(i >= n, lend loop ); 5: move(R, mem(a + i)); 6: move(i, i + 1); 7: jump lstart loop ; 8: lend loop : 9: return 10: end function Where the ; (semicolon) operator can be translated directly as s1 ; s2 seq(s1 , s2 ). Alternatively, we can also represent the structure of this code as the binary tree over seq (where a left-to-right depth-rst traversal gives the execution order) as seen in gure 1 Figure 1: Tree-like representation of our control ow seq move(i, 0) cjump(i n) move(R, mem(a + i)) move(i, i + 1) jump seq seq seq seq return

However, this way of representing the control ow is rather cumbersome, and while the arrows indicating the branches seems to be rather useful, it feels a little stupid having to tack on a seq on every single level; if only there was a way to merge these two techniques... We can just arbitrarily add in the arrows within our code listing in the semicolon notation: 2

1: 2: 3: 4: 5: 6: 7: 8: 9: 10:

function Sum(a, n) move(i, 0); lstart loop : cjump(i >= n, lend loop ); move(R, mem(a + i)); move(i, i + 1); jump lstart loop ; lend loop : return end function

but this way of representing control ow isnt as satisfactory. For example, gure 1 clearly shows the loop-structure starting at the cjump which isnt as apparent in this system. Finally, we arrive at the actual graph representing the control ow with the instructions as the nodes of the graph, presented in gure 2: Figure 2: Control Flow graph with instructions as nodes move(i, 0) cjump(i n) move(R, mem(a + i)) move(i, i + 1) jump return

In brief, a control ow graph G = (V , E), where the nodes V stmt and edges E stmt stmt is a partial function from one instruction to the next, denoting whether one instruction can step to the next within one cycle. Next, we introduce the concept of basic blocks, which are sequences that will never branch until exiting the block. For example, the sequence move(x, 0); can become a basic block because move(y, x) there are no instructions deviating the control ow within the basic block. On the other hand, the sequence there. Now, by introducing a NOP command skip, all basic blocks b seq stmt; therefore, a control ow graph can also be seen as a graph of basic blocks. For example, an equivalent graph of gure 2 may look like: move(x, 0); jump llabel ; move(y, x) cannot become a basic block because of the jump instruction in

Figure 3: Control Flow Graph whose nodes are Basic Blocks move(i, 0) cjump(i n) basic block move(R, mem(a + i)); move(i, i + 1) jump return and furthermore, an additional equivalent graph to both gure 2 and gure 3 may look like gure 4: Figure 4: Another CFG whose nodes are Basic Blocks move(i, 0) cjump(i n) if true if false move(R, mem(a + i)); move(i, i + 1); jump return

It just so happens that gure 4 generates the maximal basic block control ow (we cant coalesce any blocks together to make a larger block and still retain the original program semantic); in general, we aim to work with CFGs of maximal basic blocks.

Motivating Example

As a motivating example, we rst consider a scenario in which we want to nd the set of assignments that will never be used and hence these assignment can be considered dead-code and can hence be removed. For example, in the program Listing 2 Dead Code Removal 1: move(a, 1); 2: move(b, a + 1); 3: print(3);

we can easily see that bs value isnt needed up to the print function call (we say that b doesnt live up to print), so we can easily remove this assignment and the program would have printed the same thing. After this transformation, our program now becomes Listing 3 Dead Code Removal 1: move(a, 1); 2: print(3); Now, its fairly obvious that as value doesnt live up to the print statement either, so we can remove the assignment for a as well, rendering our nal program to just become Listing 4 Dead Code Removal 1: print(3); As demonstrated above, the ability to nd live-ranges (the range during which the value of a particular assignment is useful) is quite useful. Now, were reasonable humans, so its quite easy for us to gure out the solution to these simple little puzzles. But when the system of variables is large, as is typical within even moderately useful programs, then similar to the existence of compilers, automation may be necessary.

2.1

Live Variable Analysis

More formally, we ask to nd which subset of all possible variables are live at every program point. Program Point A program point is every single point between two instructions. For example the program points of before move(a, 0) after move(a, 0) but before move(b, 0) after move(b, 0) Live Variable A variable x is live at a program point p if the value of x is used/needed somehow along some path in the ow graph starting at p. Dead Variable On the other hand, if x is no longer needed, or if x is not live, then x is dead. So our problem now boils down to determining what variables are live for each basic block of a program. This seems like quite an easy problem to solve; we can just program in what our intuition tells us: 1. First, gather the set of used registers/variables (from now on, they are synonymous) in our program. 2. From each time a variable xi is assigned to (it is on the left hand side of a move), up until it is re-assigned again, look for the last instruction that uses it. 5 move(a, 0); move(b, 0) are the points

3. We will set xi to be live at every program point after that assignment and immediately before the last time it was used or re-assigned. From this intuition, we observe a natural constraint for determining whether a variable is live at the program point before an instruction I: The variables {x0 , x1 , , xn } that are live before instruction I must satisfy the restriction that each of xi are either live after the program point immediately after I or will be used within I; furthermore, we cannot include variables assigned to during the execution of I within our live set. (e.g. beforelive [I] = (afterlive [I] assignments(I)) + uses(I)) For example, we can try to apply the above constraints/equation to the uninitialized program: (For brevitys sake, well use the notation a := e move(a, e)) I1 : a := 0; I2 : b := 0; I3 : print(b + 3)
{}:beforelive [I1 ] {}:afterlive [I1 ] {} {} {} {}

Notice that we dont have any constraints/equations to reconcile the beforelive and afterlive of I1 and I2 . That is, once we have the information for I2 , how do we know what to put into I1 ? In this example, we can reason that since the program ows linearly, we can simply set beforelive [I2 ] = afterlive [I1 ]. That is, the live variables after executing an instruction are the same as the live variables before executing the next instruction. Since the befores and afters are the same sets, we can just remove the before sets for now. I1 : a := 0; I2 : b := 0; I3 : print(b + 3)
{} {} {} {}

Next, we notice that the information ows from afterlive to beforelive (that is, we need to know the values of after in order to gather the values of before). Therefore, we need to set an initial condition/base case for the last live-set, afterlive [I3 ]. For now, lets just assume that this is our whole program, and that no variables are live/will be used after our program terminates, which gives I1 : a := 0; I2 : b := 0; I3 : print(b + 3)
{} {} {}

Now that we have afterlive [I3 ], we can compute beforelive [I3 ]. Since I3 : print(b + 3) uses the variable b and does not assign to any variables, we say that assignments(I3 ) = uses(I3 ) = {b} Now, applying our equation beforelive [I] = (afterlive [I] assignments(I)) + uses(I) 6

to I3 gives us beforelive [I3 ] = ( ) + {b} Therefore, beforelive [I3 ] = afterlive [I2 ] = {b} I1 : a := 0; I2 : b := 0; I3 : print(b + 3) Similarly, for I2 , we have assignments(I2 ) = {b} uses(I2 ) = which gives an equation for beforelive [I2 ] = ({b} {b}) + Therefore, beforelive [I2 ] = afterlive [I1 ] = I1 : a := 0; I2 : b := 0; I3 : print(b + 3)
{} {b} {} {} {b}

Applying this equation one last time gives the nal live-set I1 : a := 0; I2 : b := 0; I3 : print(b + 3)
{b}

From this information, we can look at the rst assignment a := 0 and deduce that, because a is not live/never used after assigning to this a, then theres no point in even having this assignment, and hence we can just eliminate it. On the other hand, we see that we do need to use b after its assignment in I2 , therefore, we cannot eliminate I2 , which gives the nal transformed code (after eliminating dead code) to be b := 0; print(b + 3) Now, this is all ne and dandy if we dont have any branches or conditionals, but will our technique work within the following example? Figure 5: Find the live variables of a non-basic block program a := 1; b := 2; cjump(1 = 2) print(a); jump

print(b)

More precisely, we can no longer claim that the variables that are live after one block must be the same set of variables that are live before the next block, because we may now have multiple blocks that might come after our original block. However, if we reason that in order to eliminate the assignment to a (a is dead), we must guarantee that no code path ever uses a, then the contrapositive of that assertion will be if we can guarantee that at least one code path uses a, then we cannot eliminate the assignment to a (a is live). Since at-least-one neatly translates to set union (convince yourself of this), we can add in an additional constraint (for basic blocks at least) that afterlive [B] =
B successors(B)

beforelive [B ]

These two constraints beforelive [B] = (afterlive [B] assignments(B)) + uses(B) afterlive [B] =
B successors(B)

(1) (2)

beforelive [B ]

perfectly species the behavior of live variables, and we can use them to solve the problem in gure 5: Figure 6: Solution to gure 5 a := 1; b := 2; cjump(1 = 2)

{a}{b}

print(a); jump

{a}

print(b){b}

Which tells us that neither a nor b can be eliminated from the program.

2.2

What about loops?

Lets consider the following program

Figure 7: Live Variable Analysis in loops

a := b; b := 2; cjump(a return

2)

Introducing the desired initial conditions (namely that after returning, no variables will be used), we get Figure 8: Live Variable Analysis in loops, step 1
before[I1 ]:{} {}

I1 : a := b; I2 : b := 2; I3 : cjump(a

2)

{} after[I3 ]:{} {}

I4 : return

Applying our live variable equation to I4 , namely uses(I4 ) = assignments(I4 ) = before[I4 ] = (after[I4 ] assignments(I4 )) + uses(I4 ) = ( ) + gives the following CFG after one step Figure 9: Live Variable Analysis in loops, step 2
before[I1 ]:{} {}

I1 : a := b; I2 : b := 2; I3 : cjump(a

2)

{} after[I3 ]:{}

I4 : return

At this point, we no longer have any of the after information, so we can no longer apply 9

our primary dataow constraint (which we will call the instruction constraint). Instead, we now need to nd after[I3 ] using the information we have for before[I4 ]. Applying our second dataow constraint (which we will call the control ow constraint): afterlive [I] =
I successors(I)

beforelive [I ]

we see that we need to rst nd all successors I3 of I3 . Because I3 cjump(a 2, I4 , I1 ), which branches to I1 if a 2 and I4 otherwise, we see that the successors of I3 must be the set {I4 , I1 }, therefore, our control ow constraint applied to I3 boils down to afterlive [I3 ] = beforelive [I4 ] beforelive [I1 ] We have already calculated the value of beforelive [I4 ], so we can just plug that in. However, we dont have the value of beforelive [I1 ] yet, so we need to explicitly solve for that before we can continue. Lets try to do just that. Figure 10: Basic block of at step 2 I1 : a := b; I2 : b := 2; I3 : cjump(a
before[I1 ]:{} {}

2)

{} after[I3 ]:{}

Recall that without branching within a specic sequence of instructions (as is the case within a basic block), we can use the simplied control ow constraint, namely afterlive [Ik ] = beforelive [Ik+1 ] Finally, lets abbreviate equation (1) as the function Transfer Transfer[S, B] = (S assignments(B)) + uses(B) Lets attempt to nd the value of before[I1 ] now beforelive [I1 ] = (afterlive [I1 ] assignments(I1 )) + uses(I1 ) at this point, its fairly obvious that we need after[I1 ] as well after[I1 ] = before[I2 ] before[I2 ] = Transfer[after[I2 ], I2 ] = Transfer[before[I3 ], I2 ] = Transfer[Transfer[after[I3 ], I3 ], I2 ] uh oh At this point, we run into a slight problem. The entire point of calculating before[I1 ] was so that we can compute after[I3 ]. But in order to calculate before[I1 ], we need the value of after[I3 ] anyways... 10

Obviously, this occurs due to the fact that our program contains a loop in it. We can try to resolve this as we would a system of equations in R. For example, if we set x1 = before[I1 ] and after[I3 ] = before[I1 ] = x1 , then our equation becomes x1 = Transfer[Transfer[Transfer[x1 , I3 ], I2 ], I1 ] Now, if we think of x1 , Ik as real values and our transfer function as Transfer : R R R, then we can easily solve this system by substitution and the algebraic manipulation of the variables. For example, if the Transfer function was Transfer[x, Ik ] = 2x + k then the above system just becomes x = 2 (2 (2x + 3) + 2) + 1 = 8x + 17 17 x= 7 (We can interpret this as a graph that must satisfy the following system of equations) Figure 11: Real valued CFG and transfer function x1 : 2x2 + 1; x2 : 2x3 + 2; x3 : 2x1 + 3

Unfortunately, there are no nice analogues of this substitution technique for our sets-ofregister valued transfer functions so our traditional gradeschool algebra will not be enough to save us here. However, all is not lost. Suppose that we go back to an analogous problem Figure 12: Another real valued CFG and transfer function x1 : 2x2 ; x2 : 2x3 ; cos x1 x3 : 4

Which represents the system x = cos(x); then, if we just run this program over and over again with the initial value of 0 (that is, we plug in the value of cos(0) as x in the second iteration), we will see that its value eventually converges, or cosk (0) = cosk+1 (0) Therefore, set x = cosk (0) gives the solution, since x = cos(x) 11

Figure 13: cos(cos(cos( 0))) eventually converges

This suggests that if we just apply Transfer to itself a bunch of times with a good initial guess, it will eventually reach the desired solution. The reasoning behind this is quite natural. Suppose that our Transfer function will always transform its input into one that is better or more optimal than before, then the output will serve as a better initial guess than the previous iteration until we eventually converge towards the optimal point. Indeed, if we apply this technique to gure 10, we will see that this will produce the correct live sets within just a few iterations (termination occurs when our live-sets no longer gets updated). Lets try that now. Well start with an initial guess that no variables are live, namely beforelive [I1 ] = , then Figure 14: Initial setup of xed point solution

I1 : a := b; I2 : b := 2; I3 : cjump(a

before[I1 ]:

2)

after[I3 ]:{}

Since we dened our block-level Transfer function to be before[I1 ](k+1) = Transfer[Transfer[Transfer[before[I1 ](k) , I3 ], I2 ], I1 ] where before[I1 ](k) is the k th approximation of before[I1 ] using our xed-point iteration; we

12

can expand this out by considering all of the assignments(Ik ), uses(Ik ) as well assignments(I1 ) = {a} uses(I1 ) = {b} assignments(I2 ) = {b} uses(I2 ) = assignments(I3 ) = uses(I3 ) = {a} before[I1 ](k+1) = Transfer[Transfer[Transfer[before[I1 ](k) , I3 ], I2 ], I1 ] = Transfer[Transfer[ before[I1 ](k) + {a} , I2 ], I1 ] = Transfer[ before[I1 ](k) + {a} {b} , I1 ] = before[I1 ](k) {b} + {a} {a} + {b} = before[I1 ](k) + {b} So feeding in before[I1 ](0) = , we get before[I1 ](1) = {b} Figure 15: Step two of our xed point solution

I1 : a := b; I2 : b := 2; I3 : cjump(a

before[I1 ]:{b}

2)

after[I3 ]:{b}

Now, before[I1 ](1) is a better guess than , so we can now use it as our initial guess. However, if we attempt to take one more step however, we will nd that before[I1 ](2) = {b} + {b} = before[I1 ](1) So before[I1 ](1) is our xed point and before[I1 ] = {b} solves our constraints. From this, we can easily gather the rest of the live-sets: Figure 16: Final solution

I1 : a := b; I2 : b := 2; I3 : cjump(a

before[I1 ]:{b} {a}

2)

{a,b} after[I3 ]:{b}

I4 : return

From which we can conclude that neither I1 nor I2 are dead code. 13

Mathematical Preliminaries

You may have been slightly put o by the clever arrangement of words in section 2.2 justifying that applying the Transfer function to any input will result in an output that is always at least as close to the optimal solution as the input. However, this is obviously not true, as gure 11 demonstrates. If we have our transfer function being Transfer[x] = 8x + 17 then the sequence Transferk [0]
k=0

is the same as 17
k 0

8
k 0

which obviously diverges. We will now establish, with more mathematical rigor, the conditions under which the xedpoint of a transfer function may exist by starting o with the basics.

3.1

(Partial) Ordering

The concept of closeness surely depends on some way of ranking and ordering the variablesets such that some sets of variables are closer to the true set of live variables than others. We will embody this concept using an ordering relationship. Recall from elementary school that we can order/rank natural numbers as: 1 2 3 4 5 We extend this concept of ordering to arbitrary sets P . Lets introduce some terminology (the bread and butter of mathematicians, without which, anyone could become one) Partial Order A partial order (P , ) (or a partially ordered set) consists of A set P on which we wish to impose ordering upon An ordering (partial) relation that exhibits the properties that is: 1. Reexive : x x 2. Anti-symmetric : x y y x = x = y 3. Transitive : x y y z = x z notice the similarity between the generic and in the naturals. In fact, the naturals is itself a partially ordered set of (N, ). In fact, it is often easier to think of in terms of on some quantitative feature of our set in question. Now, we call (P , ) a partial order because not all elements of P may be comparable. We call a partial order a total order if its ordering relation obeys all of the three properties above as well as an additional fourth property 4. Totality : either x y or y x for all x, y P

14

Lets begin with a quick and dirty example. Suppose I wanted to get my friend three birthday presents. I have 6 rolls of wrapping paper, colored as {red, blue, yellow, purple, orange, green}, and I know that she prefers purple over red and blue, orange over red and yellow, and green over blue and yellow; however, she has specied no preference say, between purple and orange or red versus blue. Which three colors should I pick? We can create an order of her preference in colors ({red, blue, purple, orange, yellow, green} , ): red, blue red, yellow blue, yellow purple orange green

and Reexivity, Transitivity 3.1.1 Hasse diagrams

We can represent this graphically so items that are on the same level of ordering can be grouped together at the same height to visually indicate the ranking of the elements. This is known as a Hasse Diagram. More precisely, a Hasse Diagram is a graphical representation of a partial order where if two elements x and y P are not related by our ordering relation (i.e. we do not dene whether they should be related or not, we say that these two elements are incomparable), then they will be drawn on the same level. For example, since we do not known if the friend prefers purple over orange, these two colors are incomparable, and we would draw our chart like Figure 17: Relative positioning of purple and orange within our Hasse Diagram purple orange

x is below y when x y x red and blue below purple

y. For example, because red, blue

purple, we would put

Figure 18: red and blue are below purple in our Hasse Diagram purple red orange blue

x is connected to y by a line whenever x ywedgex y and there is no z P such that x z y. That is, x must be an immediate successor of y within our ordering hierarchy. For example, because red purple immediately, we can connect these two Figure 19: Connecting purple and red within our Hasse Diagram purple red orange blue

15

Similarly, because blue

purple and red

orange, we can connect these as well

Figure 20: More connections within our Hasse Diagram purple red orange blue

Now, suppose that we add in a new color brown, and we overhear that our friend dislikes brown as opposed to blue, then we get a new rule: brown blue

This means that we need to add it to the third level of our Hasse diagram, since brown must be below blue. Figure 21: Adding brown purple red orange blue brown Now, we know by transitivity that brown purple and brown blue, so how should we connect brown? Well, because brown isnt an immediate successor of purple (e.g. it needs to go through blue rst), we know that we cannot connect these two. Therefore, we can only connect brown to blue Figure 22: Connecting brown purple red orange blue brown In full, our hasse diagram will look like Figure 23: Full Hasse Diagram of our partial order of colors including brown purple red orange blue brown green yellow

16

From gure 23, its easy to see that brown is the least preferred color; red, blue, and yellow are next; and nally, purple, orange, and green are the most preferred colors. Therefore, we should obviously select purple, orange, and green as our wrapping paper color. As another example, the Hasse Diagram of ({0, 1, 2, 3, 4, 5, 6, 7, 8, 9} , ) looks like Figure 24: The partial ordering of naturals 9 8 7 6 5 4 3 2 1 0

3.1.2

Upper/Lower Bounds of Partial Orders

Lets now dene the concept of lower and upper bounds of a partial order. If (P , ) is a partial order and S P , then An element l P is a lower bound of S if l S, or l xx S u x S

An element u P is an upper bound of S if S

u, or x

For example, 0 is a lower bound of {1, 2, 3} Z and 4 is an upper bound. In general, there may be multiple lower and upper bounds of the same subset S. As another example, consider the same partial ordering seen before in gure 25: Figure 25: Sample Partial Order purple red orange blue brown green yellow

17

Now, we can clearly see that red is a lower bound for the subset {purple, orange} Figure 26: purple red orange blue brown However, there are no lower bounds for the subset {purple, orange, green} Figure 27: No common lower bounds for purple, orange, and green purple red orange blue brown Similarly, both blue and brown are lower bounds of the subset {blue} Figure 28: Both blue and brown are lower bounds of {blue} purple red orange blue brown green yellow green yellow green yellow

3.1.3

Least Upper Bound and Greatest Lower Bound

We will now dene the least upper bound (LUB) and the greatest lower bound (GLB). Suppose that (P , ) is a partial order and S P , then x P is the GLB of S if x is a lower bound of S and y x y LB(S), in other words, its larger than all other lower bounds of S, and is hence termed the greatest lower bound Similarly, we dene the LUB x P of S if x is the smallest upper bound of S. Unlike normal LB and U B, GLB and LU B are unique. As an example, the GLB of {purple, green, blue} is just blue

18

Figure 29: Blue is the GLB of {purple, blue, green} purple red orange blue brown while the GLB of {purple, green, blue, brown} is brown Figure 30: Blue is the GLB of {purple, blue, green} purple red orange blue brown green yellow green yellow

On the other hand, if we change our partial ordering to Figure 31: There is no GLB of {purple, orange} purple brown red orange blue green yellow

While both red, brown are lower bounds of {purple, orange}, neither are comparable. Therefore, there is no GLB for {purple, orange}

3.2

Lattices

A partial order (L, ) is a lattice if: any nite non-empty subset S L has both a LUB and a GLB in L. For example, the partial order (N, ) is a lattice because Every nite subset of N has a LUB Every (nite or innite) subset of N has a GLB On the other hand, a complete lattice is a lattice with the additional constraint that all innite subsets of L must also have both a LUB and a GLB. By this denition, (N, ) is not a complete lattice because no innite subsets of N has a LUB. 19

Figure 32: The lattice of naturals is not complete . . . 3 2 1 0 However, we can extend partial orders with two elements, = GLB(L) = LU B(L) x N, then this , such that

In the case of the naturals = 0; but what about ? Instead, suppose we extend the naturals with a top elements such that x augmented set is complete (i.e. it contains a LUB and GLB) Figure 33: The lattice of N is complete

. . . 3 2 1 0 From now on, we will almost exclusively work with complete lattices, so lets take a look at a complete lattice of a set of 3 elements on the ordering operator . Figure 34: The complete lattice of ({x, y, z} , ) : {a, b, c} {a, b} {a} {a, c} {b} : {b, c} {c}

20

You might also like