Compiler Construction Compiler Construction 1 / 54 Mayer Goldberg \ - - PowerPoint PPT Presentation

compiler construction
SMART_READER_LITE
LIVE PREVIEW

Compiler Construction Compiler Construction 1 / 54 Mayer Goldberg \ - - PowerPoint PPT Presentation

Compiler Construction Compiler Construction 1 / 54 Mayer Goldberg \ Ben-Gurion University Tuesday 10 th December, 2019 Mayer Goldberg \ Ben-Gurion University Chapter 5 Agenda tail-call-optimization Compiler Construction 2 / 54 Intuition


slide-1
SLIDE 1

Compiler Construction

Mayer Goldberg \ Ben-Gurion University Tuesday 10th December, 2019

Mayer Goldberg \ Ben-Gurion University Compiler Construction 1 / 54

slide-2
SLIDE 2

Chapter 5

Agenda

▶ Intuition about the tail-calls, tail-position, & the

tail-call-optimization

▶ The tail-position, tail-call ▶ The TCO ▶ Loops & tail-recursion ▶ Annotating the tail-call ▶ What TCO code looks like ▶ Implementing the TCO

Mayer Goldberg \ Ben-Gurion University Compiler Construction 2 / 54

slide-3
SLIDE 3

The tail-call (intuitively)

▶ Two people are walking through a forest, and encounter a witch

🧚

▶ The witch grants them three wishes… ▶ Here is what each person wished for:

First Person Second Person US$1,000,000 3 more wishes A grade of 100 US$1,000,000 3 more wishes A grade of 100

▶ What is the difgerence?

Mayer Goldberg \ Ben-Gurion University Compiler Construction 3 / 54

slide-4
SLIDE 4

The tail-call (continued)

▶ Here is what each person wished for:

First Person Second Person US$1,000,000 3 more wishes A grade of 100 US$1,000,000 3 more wishes A grade of 100

▶ The fjrst person’s wishes are simple to grant ▶ The second person’s wishes are annoying:

▶ The witch must remember what to do once she returned from

granting the 3 new wishes… She still has work to do!

▶ Since this nonsense is going to go on for a while, the witch

needs a stack of paper slips to manage the outstanding requests, in order…

This is the difgerence between a tail-call (First Person) & a non-tail-call (Second Person)

Mayer Goldberg \ Ben-Gurion University Compiler Construction 4 / 54

slide-5
SLIDE 5

The tail-call (continued)

Back to reality:

▶ Non-tail-calls require additional stack frames to manage

arguments & return addresses

▶ The stack depth (in frames) is proportional to the number of

non-tail-calls

▶ Tail-calls do not require additional stack frames

▶ The stack depth (in frames) is independent of the number of

tail-calls

Mayer Goldberg \ Ben-Gurion University Compiler Construction 5 / 54

slide-6
SLIDE 6

The tail-call optimization (intuitively)

▶ You are surfjng a web browser ▶ The browser is broken: It has no ⟨Back⟩ key

▶ Once you click on a link, you are unable to return

▶ To read a web page, you therefore right-click on a link & select

the option to open in a new frame

▶ You read the page in the new frame, possibly opening links in

additional frames

▶ When you are done reading a page, you click on the ⊠ button

to remove the frame

Mayer Goldberg \ Ben-Gurion University Compiler Construction 6 / 54

slide-7
SLIDE 7

The tail-call optimization (intuitively, cont)

Mayer Goldberg \ Ben-Gurion University Compiler Construction 7 / 54

slide-8
SLIDE 8

The tail-call optimization (intuitively, cont)

▶ Some web pages have special links:

▶ These links are not just the last links on the page ▶ These links are the last thing on the page ▶ There’s nothing to read past these links!

☞ Not all pages have such special links! This page does:

Mayer Goldberg \ Ben-Gurion University Compiler Construction 8 / 54

slide-9
SLIDE 9

The tail-call optimization (intuitively, cont)

☞ This page does not have such a special link. The last link on the

page is not the last thing on the page, so returning from that link, there is yet something to read:

🤕 Yeah? What’s there left to read???

▶ Enjoy the logo; It is not a link! 😊 Mayer Goldberg \ Ben-Gurion University Compiler Construction 9 / 54

slide-10
SLIDE 10

The tail-call optimization (intuitively, cont)

▶ Because there’s nothing to read past these links, we need

neither to open a new frame nor to return from it:

▶ Rather than right-click & open the page in the new frame, we

simply click on the link in place

▶ The new contents shall overwrite the old contents ▶ The new contents can be larger or smaller than the contents it

replaces

▶ The size of the frame can change ▶ The number of frames shall not change ▶ When we’re done reading the page, we close it with ⊠ Mayer Goldberg \ Ben-Gurion University Compiler Construction 10 / 54

slide-11
SLIDE 11

The tail-call optimization (intuitively, cont)

The tail-calls

a new frame a new frame a new frame

Mayer Goldberg \ Ben-Gurion University Compiler Construction 11 / 54

slide-12
SLIDE 12

Chapter 5

Agenda 🗹 Intuition about the tail-calls, tail-position, & the

tail-call-optimization

▶ The tail-position, tail-call ▶ The TCO ▶ Loops & tail-recursion ▶ Annotating the tail-call ▶ What TCO code looks like ▶ Implementing the TCO

Mayer Goldberg \ Ben-Gurion University Compiler Construction 12 / 54

slide-13
SLIDE 13

The tail-position

▶ The tail-position is a point in the body of a function, procedure,

method, subroutine, etc., just before the return-statement, where the last computation is performed

▶ Exception handling: Similarly to return-statements,

raise/throw-statements also identify tail-positions!

▶ Because computation may proceed non-linearly, there many be

more than one tail-position

▶ To fjnd the tail positions, fjnd all points in the code where a

return-statement or the ret instruction could be placed

Mayer Goldberg \ Ben-Gurion University Compiler Construction 13 / 54

slide-14
SLIDE 14

The tail-position (continued)

Example:

(lambda (x) (f (g (g x))))

tail-call

Mayer Goldberg \ Ben-Gurion University Compiler Construction 14 / 54

slide-15
SLIDE 15

The tail-position (continued)

Example:

(lambda (x) (f (lambda (y) (g x y))))

tail-calls

☞ Each lambda-expression has its own return-statement, and

therefore, each lambda-expression has its own tail-position!

Mayer Goldberg \ Ben-Gurion University Compiler Construction 15 / 54

slide-16
SLIDE 16

The tail-position (continued)

Example:

(lambda (x y z w) (if (foo? x) (goo y) (boo (doo z))))

tail-calls

☞ If an if-expression is in tail-position, then the then-expression

& else-expression are also in tail-position

Mayer Goldberg \ Ben-Gurion University Compiler Construction 16 / 54

slide-17
SLIDE 17

The tail-position (continued)

Example:

(lambda (x y z) (f (if (g? x) (h y) (w z))))

tail-call

☞ If an if-expression is not in tail-position, then neither are its

then-expression & else-expression

Mayer Goldberg \ Ben-Gurion University Compiler Construction 17 / 54

slide-18
SLIDE 18

The tail-position (continued)

Example:

(lambda (a b) (f a) (g a b) (display "done!\n"))

tail-call

☞ If a sequence, whether explicit or implicit, is in tail-position, the

last expression in the sequence is also in tail-position

Mayer Goldberg \ Ben-Gurion University Compiler Construction 18 / 54

slide-19
SLIDE 19

The tail-position (continued)

Example:

(lambda () (and (f x) (g y) (h z)))

▶ tail-call

☞ If an and-expression is in tail-position then its last expression is

also in tail-position

▶ While it is possible to return after computing previous

expressions [within an and-expression], it is only from the last expression that return is possible immediately, without fjrst testing the value of the expression

Mayer Goldberg \ Ben-Gurion University Compiler Construction 19 / 54

slide-20
SLIDE 20

The tail-position (continued)

Example:

(lambda () (or (f (g x)) y))

▶ The above example contains no application in tail-position

☞ Similarly to and-expression, if an or-expression is in tail-position

then its last expression is in tail-position

Mayer Goldberg \ Ben-Gurion University Compiler Construction 20 / 54

slide-21
SLIDE 21

The tail-position (continued)

Example:

(lambda () (set! x (f y)))

☞ The body of a set!-expression is never in tail-position!

Mayer Goldberg \ Ben-Gurion University Compiler Construction 21 / 54

slide-22
SLIDE 22

The tail-position (continued)

Example:

(lambda () (set! x (f (lambda (y) (g x y)))))

▶ tail-call

☞ Even though the body of the set!-expression is not in

tail-position, “every lambda-expression has its own return”

▶ The marked application is in tail-position relative to its

enclosing lambda-expression

Mayer Goldberg \ Ben-Gurion University Compiler Construction 22 / 54

slide-23
SLIDE 23

The tail-position (continued)

Example:

(lambda (x y z) (cond ((f? x) (g y)) ((g? x) (f x) (f y)) (else (h x) (f y) (g (f x)))))

▶ tail-calls

☞ If a cond-expression is in tail-position then the last expression in

the implicit sequence of each cond-rib is also in tail-position

Mayer Goldberg \ Ben-Gurion University Compiler Construction 23 / 54

slide-24
SLIDE 24

The tail-position (continued)

Example:

(let ((x (f y)) (y (g x))) (goo (boo x) y))

tail-call

☞ The body of a let-expression is the body of a

lambda-expression.

▶ Therefore, the last expression in the implicit sequence of the

body is in tail-position

Mayer Goldberg \ Ben-Gurion University Compiler Construction 24 / 54

slide-25
SLIDE 25

The tail-position (continued)

Example:

(lambda (s) (apply f s)) This is an interesting example:

▶ On the one hand, there is only one tail-call that appears

statically in the source code

▶ On the other hand, there is another tail-call that shall take place

at run-time: The application of f must re-use the top activation frame!

☞ The implementation of apply duplicates part of the code for

the tail-call-optimization, so that the frame for the call to f

  • verwrites the frame for the call to apply!

Mayer Goldberg \ Ben-Gurion University Compiler Construction 25 / 54

slide-26
SLIDE 26

The tail-position (continued)

Example:

let disj nt1 nt2 s = try (nt1 s) with X_no_match -> (nt2 s);;

▶ This applicatoin is not in tail-position

🤕 Upon returning successfully, the code needs to pop an

exception-handler!

▶ This applicatoin is in tail-position

Mayer Goldberg \ Ben-Gurion University Compiler Construction 26 / 54

slide-27
SLIDE 27

Chapter 5

Agenda 🗹 Intuition about the tail-calls, tail-position, & the

tail-call-optimization

🗹 The tail-position, tail-call

▶ The TCO ▶ Loops & tail-recursion ▶ Annotating the tail-call ▶ What TCO code looks like ▶ Implementing the TCO

Mayer Goldberg \ Ben-Gurion University Compiler Construction 27 / 54

slide-28
SLIDE 28

The tail-call optimization

The tail-call optimization is a recycling of activation frames on the stack:

▶ Function/method calls ordinarily open new activation frames on

the stack

▶ Function/method calls in tail-position need not open new

activation frames, but may re-use the current/top frame

▶ Only two items in the current activation frame need to be

preserved during a tail-call:

▶ The old frame-pointer ▶ The return-address

▶ Everything else on the stack —

▶ The lexical environment ▶ The values of arguments ▶ Any local values

are all overwritten & replaced by the contents of the new frame

Mayer Goldberg \ Ben-Gurion University Compiler Construction 28 / 54

slide-29
SLIDE 29

The tail-call optimization (continued)

▶ The tail-call optimization optimizes stack frames

▶ In some situations this may also optimize time ▶ We shall not encounter many other optimizations of space ▶ The tail-call optimization is very important! Mayer Goldberg \ Ben-Gurion University Compiler Construction 29 / 54

slide-30
SLIDE 30

Chapter 5

Agenda 🗹 Intuition about the tail-calls, tail-position, & the

tail-call-optimization

🗹 The tail-position, tail-call 🗹 The TCO

▶ Loops & tail-recursion ▶ Annotating the tail-call ▶ What TCO code looks like ▶ Implementing the TCO

Mayer Goldberg \ Ben-Gurion University Compiler Construction 30 / 54

slide-31
SLIDE 31

The tail-call optimization (continued)

The signifjcance of the TCO

▶ All loops are special cases of recursive functions where the

recursive call is in tail-position

▶ The TCO is our license to use recursive procedures to

implement iteration

▶ Absent the TCO, iteration using recursion would be

prohibitively expensive:

⚠ The amount of stack consumed in the execution of a loop

would be proportional to the number of iterations

⚠ Large loops would exhaust the stack

Mayer Goldberg \ Ben-Gurion University Compiler Construction 31 / 54

slide-32
SLIDE 32

Loops

▶ By now, we know how to identify the tail-position ▶ We made the claim that the TCO is important because it gives

us license to implement loops using tail-recursive functions

☞ We now need to pay this debt, and show that loops are

tail-recursive functions!

Mayer Goldberg \ Ben-Gurion University Compiler Construction 32 / 54

slide-33
SLIDE 33

Loops (continued)

Example: while-loops

(define while (lambda (test body) (if (test) (begin (body) (while test body)))))

tail-call

Mayer Goldberg \ Ben-Gurion University Compiler Construction 33 / 54

slide-34
SLIDE 34

Loops (continued)

Example: while-loops (cont)

> (let ((i 0)) (while (lambda () (< i 10)) (lambda () (set! i (+ 1 i)) (display (format "~a~%" i))))) 1 2 ... 9 10

Mayer Goldberg \ Ben-Gurion University Compiler Construction 34 / 54

slide-35
SLIDE 35

Loops (continued)

Example: for-loops

(define for (lambda (from to body) (if (< from to) (begin (body from) (for (+ 1 from) to body)))))

▶ Notice that the body is parameterized by the index-variable of

the loop

tail-call

Mayer Goldberg \ Ben-Gurion University Compiler Construction 35 / 54

slide-36
SLIDE 36

Loops (continued)

Example: for-loops (cont)

> (for 1 6 (lambda (i) (for 1 6 (lambda (j) (display (format "\t~a" (* i j))))) (newline))) 1 2 3 4 5 2 4 6 8 10 3 6 9 12 15 4 8 12 16 20 5 10 15 20 25

Mayer Goldberg \ Ben-Gurion University Compiler Construction 36 / 54

slide-37
SLIDE 37

The tail-call optimization (continued)

▶ The tail-call optimization is not just important for loops ▶ Consider Ackermann’s Function:

Ack(0, q) = q + 1 Ack(p + 1, 0) = Ack(p, 1) Ack(p + 1, q + 1) = Ack(p, Ack(p + 1, q)) Ack(2, 2) = 7 Ack(3, 3) = 61

▶ tail-calls ▶ non-tail-call ▶ The TCO cuts reduces the number of frames needed by 50%,

which lets us compute Ackermann’s Function for larger inputs

Mayer Goldberg \ Ben-Gurion University Compiler Construction 37 / 54

slide-38
SLIDE 38

The tail-call optimization (continued)

One disadvantage of the TCO:

▶ When a frame is overwritten, debug information is lost ▶ Smalltalk & Java have great debuggers

▶ Scheme does not!

▶ Language implementations that do not implement the TCO

should, at the very least, ofger effjcient & convenient looping mechanisms

☞ Compromise: Turn the TCO on/ofg while debugging

▶ No Scheme compiler does this ▶ It’s easy (if tedious) to by hand:

(define id (lambda (x) x)) Just wrap each tail-call with id ☺

Mayer Goldberg \ Ben-Gurion University Compiler Construction 38 / 54

slide-39
SLIDE 39

Chapter 5

Agenda 🗹 Intuition about the tail-calls, tail-position, & the

tail-call-optimization

🗹 The tail-position, tail-call 🗹 The TCO 🗹 Loops & tail-recursion

▶ Annotating the tail-call ▶ What TCO code looks like ▶ Implementing the TCO

Mayer Goldberg \ Ben-Gurion University Compiler Construction 39 / 54

slide-40
SLIDE 40

Annotating tail-calls in our compiler

▶ Annotating tail-calls is done in a single pass over the AST of

expr

▶ You are given the type expr' which includes the type

constructor ApplicTP' of expr' * (expr' list) for encoding tail-calls

▶ The simplest way to annotate tail-calls is to carry along an

auxiliary parameter in_tp (read: in tail-position) to indicate whether the current expression is in tail-position

▶ The initial value of in_tp is false ▶ When an Appic is encountered and the value of in_tp is true,

an ApplicTP' is used to package the result of the recursive calls over the procedure and the list of arguments

▶ Upon entering lambda-expressions (of any kind), the value of

in_tp is reset back to true

Mayer Goldberg \ Ben-Gurion University Compiler Construction 40 / 54

slide-41
SLIDE 41

Putting it all together…

(lambda (a) (a (a (lambda (b) (b (b (a c))))))) LambdaSimple ' (["a"], ApplicTP ' (Var' (VarParam ' ("a", 0)), [Applic ' (Var' (VarParam ' ("a", 0)), [LambdaSimple ' (["b"], ApplicTP ' (Var' (VarParam ' ("b", 0)), [Applic ' (Var' (VarParam ' ("b", 0)), [Applic ' (Var' (VarBound ' ("a", 0, 0)), [Var' (VarFree ' "c")])])]))])]))

Mayer Goldberg \ Ben-Gurion University Compiler Construction 41 / 54

slide-42
SLIDE 42

Chapter 5

Agenda 🗹 Intuition about the tail-calls, tail-position, & the

tail-call-optimization

🗹 The tail-position, tail-call 🗹 The TCO 🗹 Loops & tail-recursion 🗹 Annotating the tail-call

▶ What TCO code looks like ▶ Implementing the TCO

Mayer Goldberg \ Ben-Gurion University Compiler Construction 42 / 54

slide-43
SLIDE 43

TCO Ackermann in C

The TCO is simplest to demonstrate on a function with immediate tail-recursion:

▶ The new frame is identical to the old frame, both in size and

types Consider Ackermann’s Function: int ack(int a, int b) { if (a == 0) { return b + 1; } else if (b == 0) { return ack(a - 1, 1); } else { return ack(a - 1, ack(a, b - 1)); } }

🤕 A tail call is identifjed by the pattern return f(...) ☞ There are two tail-recursive function calls

Mayer Goldberg \ Ben-Gurion University Compiler Construction 43 / 54

slide-44
SLIDE 44

TCO Ackermann in C (continued)

Optimizing the tail-calls in Ackermann’s Function results in: int ack(int a, int b) { L: if (a == 0) { return b + 1; } else if (b == 0) { b = 1; --a; goto L; } else { b = ack(a, b - 1); --a; goto L; } }

▶ Notice that the current frame is changed in place ▶ Notice that goto replaces the function call ▶ Notice that one non-tail-call remains

Mayer Goldberg \ Ben-Gurion University Compiler Construction 44 / 54

slide-45
SLIDE 45

TCO Ackermann in x86/64

Here is Ackermann’s Function, with the TCO, in x86/64: ack: .A: cmp rdi, 0 lea rax, [rsi + 1] jz .A ret cmp rsi, 0 jz .B .B: push rdi dec rdi dec rsi mov rsi, 1 call ack jmp ack mov rsi, rax pop rdi dec rdi jmp ack Tail-calls and a non-tail-call

Mayer Goldberg \ Ben-Gurion University Compiler Construction 45 / 54

slide-46
SLIDE 46

The tail-call optimization (continued)

▶ When the tail-call is not immediately-recursive

▶ non-recursive, or ▶ mutually-recursive

we cannot demonstrate the TCO in high-level C

▶ This would require that we goto from within the body of one

procedure into a label that is local to another procedure

▶ The new frame can be very difgerent in size, argument count,

and argument type from the old frame, so we cannot overwrite it in a high-level language

Mayer Goldberg \ Ben-Gurion University Compiler Construction 46 / 54

slide-47
SLIDE 47

The tail-position (continued)

▶ Tail-recursion, whether immediate or mutual, is a special case of

the tail-call optimization, where the call is recursive or mutually recursive

▶ The general tail-recursion optimization is all that is required to

support the implementation of loops using recursion

▶ This is required by the standard for Scheme

▶ The tail-call optimization is more general than the tail-recursion

  • ptimization, and optimizes all tail-calls

▶ The tail-call optimization crosses the boundaries of

functions/procedures/methods, and is implemented in assembly/machine language

☞ You will implement the tail-call-optimization in your compilers

Mayer Goldberg \ Ben-Gurion University Compiler Construction 47 / 54

slide-48
SLIDE 48

Chapter 5

Agenda 🗹 Intuition about the tail-calls, tail-position, & the

tail-call-optimization

🗹 The tail-position, tail-call 🗹 The TCO 🗹 Loops & tail-recursion 🗹 Annotating the tail-call 🗹 What TCO code looks like

▶ Implementing the TCO

Mayer Goldberg \ Ben-Gurion University Compiler Construction 48 / 54

slide-49
SLIDE 49

The tail-call optimization (continued)

Upon tail-call ① Evaluate the arguments, and push their values onto the stack in

reverse order (from last to fjrst)

② Push the argument count ③ Evaluate the procedure expression

▶ Verify that we have a closure!

④ Push the lexical environment of the closure ⑤ Push the return address of the current frame ⑥ Restore the old frame-pointer register (on x86/64: rbp) ⑦ Overwrite the existing frame with the new frame

▶ Loop, memcpy, whatever…

⑧ jmp to the code-pointer of the closure

Mayer Goldberg \ Ben-Gurion University Compiler Construction 49 / 54

slide-50
SLIDE 50

The tail-call optimization (continued)

Upon return from a procedure-call ① Add a wordsize (8 bytes) to the stack-pointer (on x86/64: rsp)

to move past the lexical environment

② Pop ofg the argument count

▶ This can be difgerent from the number of arguments you

pushed:

▶ If you call a procedure with n arguments and it tail-calls a

procedure with k arguments, and it calls a procedure with r arguments, which returns, then you get back r arguments on the stack!

▶ Variadic lambda-expressions and lambda-expressions with

  • ptional arguments will alter the stack to number of their

parameters: If you call the procedure (lambda (a b c . r) ... ) with 29 arguments, and it returns, you can expect 4 arguments on the stack, the last one of which being a list of the remaining 26 arguments…

Mayer Goldberg \ Ben-Gurion University Compiler Construction 50 / 54

slide-51
SLIDE 51

The tail-call optimization (continued)

Upon return from a procedure-call ② Pop ofg the argument count ③ Remove as many arguments ofg the stack as indicated by the

argument count: Add word-size * argument-count to the stack-pointer register

Mayer Goldberg \ Ben-Gurion University Compiler Construction 51 / 54

slide-52
SLIDE 52

Comparing stacks for non-TCs and TCs

A-n-1 A-n-2 ⋯ A-0 n lex-env-g ret-to-f B-m-1 B-m-2 B-m-3 ⋯ B-0 m lex-env-h rbp-in-f A [non-tail] call to (g A-0 ⋯ A-n-1) from within the body

  • f the procedure f

Frame of the call (g A-0 ⋯ A-n-1) ret-to-g Frame of the call (h B-0 ... B-m-1) Setting up the stack for a [non-tail] call (h B-0 ⋯ B-m-1) from within the body

  • f the procedure g

Once called, h shall push the rbp... Setting up the stack for a non-tail call A-n-1 A-n-2 ⋯ A-0 n lex-env-g ret-to-f B-m-1 B-m-2 B-m-3 ⋯ B-0 m lex-env-h rbp-in-f A [non-tail] call to (g A-0 ⋯ A-n-1) from within the body

  • f the procedure f

Frame of the call (g A-0 ⋯ A-n-1) ret-to-f Setting up the stack for a tail-call (h B-0 ⋯ B-m-1) from within the body

  • f the procedure g

Once called, h shall push the rbp... Frame of the call (h B-0 ... B-m-1) B-m-1 B-m-2 B-m-3 B-0 m lex-env-h ret-to-f The stack set up for the tail-call (h B-0 ⋯ B-m-1) after overwriting the frame from the call to g, and before jumping to f Setting up the stack for a tail-call

Mayer Goldberg \ Ben-Gurion University Compiler Construction 52 / 54

slide-53
SLIDE 53

Chapter 5

Agenda 🗹 Intuition about the tail-calls, tail-position, & the

tail-call-optimization

🗹 The tail-position, tail-call 🗹 The TCO 🗹 Loops & tail-recursion 🗹 Annotating the tail-call 🗹 What TCO code looks like 🗹 Implementing the TCO

Mayer Goldberg \ Ben-Gurion University Compiler Construction 53 / 54

slide-54
SLIDE 54

Further reading

Mayer Goldberg \ Ben-Gurion University Compiler Construction 54 / 54