Placeholder Image

字幕列表 影片播放

  • [MUSIC PLAYING]

  • PROFESSOR: Last time, we took a look at an explicit control

  • evaluator for Lisp, and that bridged the gap between all

  • these high-level languages like Lisp and the query

  • language and all of that stuff, bridged the gap between

  • that and a conventional register machine.

  • And in fact, you can think of the explicit control evaluator

  • either as, say, the code for a Lisp interpreter if you wanted

  • to implement it in the assembly language of some

  • conventional register transfer machine, or, if you like, you

  • can think of it as the microcode of some machine

  • that's going to be specially designed to run Lisp.

  • In either case, what we're doing is we're taking a

  • machine that speaks some low-level language, and we're

  • raising the machine to a high-level language like Lisp

  • by writing an interpreter.

  • So for instance, here, conceptually, is a special

  • purpose machine for computing factorials.

  • It takes in five and puts out 120.

  • And what this special purpose machine is is actually a Lisp

  • interpreter that's configured itself to run factorials,

  • because you fit into it a description of

  • the factorial machine.

  • So that's what an interpreter is.

  • It configures itself to emulate a machine whose

  • description you read in.

  • Now, inside the Lisp interpreter, what's that?

  • Well, that might be your general register language

  • interpreter that configures itself to behave like a Lisp

  • interpreter, because you put in a whole bunch of

  • instructions in register language.

  • This is the explicit control evaluator.

  • And then it also has some sort of library, a library of

  • primitive operators and Lisp operations and all sorts of

  • things like that.

  • That's the general strategy of interpretation.

  • And the point is, what we're doing is we're writing an

  • interpreter to raise the machine to the level of the

  • programs that we want to write.

  • Well, there's another strategy, a different one,

  • which is compilation.

  • Compilation's a little bit different.

  • Here--here we might have produced a special purpose

  • machine for, for computing factorials, starting with some

  • sort of machine that speaks register language, except

  • we're going to do a different strategy.

  • We take our factorial program.

  • We use that as the source code into a compiler.

  • What the compiler will do is translate that factorial

  • program into some register machine language.

  • And this will now be not the explicit control evaluator for

  • Lisp, this will be some register language for

  • computing factorials.

  • So this is the translation of that.

  • That will go into some sort of loader which will combine this

  • code with code selected from the library to do things like

  • primitive multiplication.

  • And then we'll produce a load module which configures the

  • register language machine to be a special

  • purpose factorial machine.

  • So that's a, that's a different strategy.

  • In interpretation, we're raising the machine to the

  • level of our language, like Lisp.

  • In compilation, we're taking our program and lowering it to

  • the language that's spoken by the machine.

  • Well, how do these two strategies compare?

  • The compiler can produce code that will execute more

  • efficiently.

  • The essential reason for that is that if you think about the

  • register operations that are running, the interpreter has

  • to produce register operations which, in principle, are going

  • to be general enough to execute any Lisp procedure.

  • Whereas the compiler only has to worry about producing a

  • special bunch of register operations for, for doing the

  • particular Lisp procedure that you've compiled.

  • Or another way to say that is that the interpreter is a

  • general purpose simulator, that when you read in a Lisp

  • procedure, then those can simulate the program described

  • by that, by that procedure.

  • So the interpreter is worrying about making a general purpose

  • simulator, whereas the compiler, in effect, is

  • configuring the thing to be the machine that the

  • interpreter would have been simulating.

  • So the compiler can be faster.

  • On the other hand, the interpreter is a nicer

  • environment for debugging.

  • And the reason for that is that we've got the source code

  • actually there.

  • We're interpreting it.

  • That's what we're working with.

  • And we also have the library around.

  • See, the interpreter--the library sitting there is part

  • of the interpreter.

  • The compiler only pulls out from the library what it needs

  • to run the program.

  • So if you're in the middle of debugging, and you might like

  • to write a little extra program to examine some run

  • time data structure or to produce some computation that

  • you didn't think of when you wrote the program, the

  • interpreter can do that perfectly well, whereas the

  • compiler can't.

  • So there are sort of dual, dual advantages.

  • The compiler will produce code that executes faster.

  • The interpreter is a better environment for debugging.

  • And most Lisp systems end up having both, end up being

  • configured so you have an interpreter that you use when

  • you're developing your code.

  • Then you can speed it up by compiling.

  • And very often, you can arrange that compiled code and

  • interpreted code can call each other.

  • We'll see how to do that.

  • That's not hard.

  • In fact, the way we'll--

  • in the compiler we're going to make, the way we'll arrange

  • for compiled coding and interpreted code to call, to

  • call each other, is that we'll have the compiler use exactly

  • the same register conventions as the interpreter.

  • Well, the idea of a compiler is very much like the idea of

  • an interpreter or evaluator.

  • It's the same thing.

  • See, the evaluator walks over the code and performs some

  • register operations.

  • That's what we did yesterday.

  • Well, the compiler essentially would like to walk over the

  • code and produce the register operations that the evaluator

  • would have done were it evaluating the thing.

  • And that gives us a model for how to implement a

  • zeroth-order compiler, a very bad compiler but

  • essentially a compiler.

  • A model for doing that is you just take the evaluator, you

  • run it over the code, but instead of executing the

  • actual operations, you just save them away.

  • And that's your compiled code.

  • So let me give you an example of that.

  • Suppose we're going to compile--suppose we want to

  • compile the expression f of x.

  • So let's assume that we've got f of x in the x register and

  • something in the environment register.

  • And now imagine starting up the evaluator.

  • Well, it looks at the expression and it sees that

  • it's an application.

  • And it branches to a place in the evaluator code we saw

  • called ev-application.

  • And then it begins.

  • It stores away the operands and unev, and then it's going

  • to put the operator in exp, and it's going to go

  • recursively evaluate it.

  • That's the process that we walk through.

  • And if you start looking at the code, you start seeing

  • some register operations.

  • You see assign to unev the operands, assign to exp the

  • operator, save the environment, generate

  • that, and so on.

  • Well, if we look on the overhead here, we can see, we

  • can see those operations starting to be produced.

  • Here's sort of the first real operation that the evaluator

  • would have done.

  • It pulls the operands out of the exp register and assigns

  • it to unev. And then it assigns something to the

  • expression register, and it saves continue, and it saves

  • env.

  • And all I'm doing here is writing down the register

  • assignments that the evaluator would have done in

  • executing that code.

  • And can zoom out a little bit.

  • Altogether, there are about 19 operations there.

  • And this is the--this will be the piece of code up until the

  • point where the evaluator branches off to

  • apply-dispatch.

  • And in fact, in this compiler, we're not going to worry about

  • apply-dispatch at all.

  • We're going to have everything--we're going to

  • have both interpreted code and compiled code.

  • Always evaluate procedures, always apply procedures by

  • going to apply-dispatch.

  • That will easily allow interpreted code and compiled

  • code to call each other.

  • Well, in principle, that's all we need to do.

  • You just run the evaluator.

  • So the compiler's a lot like the evaluator.

  • You run it, except it stashes away these operations instead

  • of actually executing them.

  • Well, that's not, that's not quite true.

  • There's only one little lie in that.

  • What you have to worry about is if you have a, a predicate.

  • If you have some kind of test you want to do, obviously, at

  • the point when you're compiling it, you don't know

  • which branch of these--of a conditional like this you're

  • going to do.

  • So you can't say which one the evaluator would have done.

  • So all you do there is very simple.

  • You compile both branches.

  • So you compile a structure that looks like this.

  • That'll compile into something that says, the code, the code

  • for P. And it puts its results in, say, the val register.

  • So you walk the interpreter over the predicate and make

  • sure that the result would go into the val register.

  • And then you compile an instruction that says, branch

  • if, if val is true, to a place we'll call label one.

  • Then we, we will put the code for B to walk the

  • interpreter--walk the interpreter over B. And then

  • go to put in an instruction that says, go to the next

  • thing, whatever, whatever was supposed to happen after this

  • thing was done.

  • You put in that instruction.

  • And here you put label one.

  • And here you put the code for A. And you

  • put go to next thing.

  • So that's how you treat a conditional.

  • You generate a little block like that.

  • And other than that, this zeroth-order compiler is the

  • same as the evaluator.

  • It's just stashing away the instructions instead of

  • executing them.

  • That seems pretty simple, but we've gained

  • something by that.

  • See, already that's going to be more

  • efficient than the evaluator.

  • Because, if you watch the evaluator run, it's not only

  • generating the register operations we wrote down, it's

  • also doing things to decide which ones to generate.

  • So the very first thing it does, say, here for instance,

  • is go do some tests and decide that this is an application,

  • and then branch off to the place that, that handles

  • applications.

  • In other words, what the evaluator's doing is

  • simultaneously analyzing the code to see what to do, and

  • running these operations.

  • And when you--

  • if you run the evaluator a million times, that analysis

  • phase happens a million times, whereas in the compiler, it's

  • happened once, and then you just have the register

  • operations themselves.

  • Ok, that's a, a zeroth-order compiler, but it is a

  • wretched, wretched compiler.

  • It's really dumb.

  • Let's--let's go back and, and look at this overhead.

  • So look at look at some of the operations

  • this thing is doing.

  • We're supposedly looking at the operations and

  • interpreting f of x.

  • Now, look here what it's doing.

  • For example, here it assigns to exp the

  • operator in fetch of exp.

  • But see, there's no reason to do that, because this is--

  • the compiler knows that the operator, fetch of exp, is f

  • right here.

  • So there's no reason why this instruction should say that.

  • It should say, we'll assign to exp, f.

  • Or in fact, you don't need exp at all.

  • There's no reason it should have exp at all.

  • What, what did exp get used for?

  • Well, if we come down here, we're going to assign to val,

  • look up the stuff in exp in the environment.

  • So what we really should do is get rid of the exp register

  • altogether, and just change this instruction to say,

  • assign to val, look up the variable value of the symbol f

  • in the environment.

  • Similarly, back up here, we don't need unev at all,

  • because we know what the operands of fetch of exp are

  • for this piece of code.

  • It's the, it's the list x.

  • So in some sense, you don't want unev and exp at all.

  • See, what they really are in some sense, those aren't

  • registers of the actual machine

  • that's supposed to run.

  • Those are registers that have to do with arranging the thing

  • that can simulate that machine.

  • So they're always going to hold expressions which, from

  • the compiler's point of view, are just constants, so can be

  • put right into the code.

  • So you can forget about all the operations worrying about

  • exp and unev and just use those constants.

  • Similarly, again, if we go, go back and look here, there are

  • things like assign to continue eval-args.

  • Now, that has nothing to do with anything.

  • That was just the evaluator keeping track of where it

  • should go next, to evaluate the arguments in some, in some

  • application.

  • But of course, that's irrelevant to the compiler,

  • because you--

  • the analysis phase will have already done that.

  • So this is completely irrelevant.

  • So a lot of these, these assignments to continue have

  • not to do where the running machine is supposed to

  • continue in keeping track of its state.

  • It has to, to do with where the evaluator analysis should

  • continue, and those are completely irrelevant.

  • So we can get rid of them.

  • Ok, well, if we, if we simply do that, make those kinds of

  • optimizations, get rid, get rid of worrying about exp and

  • unev, and get rid of these irrelevant register

  • assignments to continue, then we can take this literal code,

  • these sort of 19 instructions that the, that the evaluator

  • would have done, and then replace them.

  • Let's look at the, at the slide.

  • Replace them by--we get rid of about half of them.

  • And again, this is just sort of filtering what the

  • evaluator would have done by getting rid of

  • the irrelevant stuff.

  • And you see, for instance, here the--where the evaluator

  • said, assign val, look up variable value, fetch of exp,

  • here we have put in the constant f.

  • Here we've put in the constant x.

  • So there's a, there's a little better compiler.

  • It's still pretty dumb.

  • It's still doing a lot of dumb things.

  • Again, if we go look at the slide again, look at the very

  • beginning here, we see a save the environment, assign

  • something to the val register, and restore the environment.

  • Where'd that come from?

  • That came from the evaluator back here saying, oh, I'm in

  • the middle of evaluating an application.

  • So I'm going to recursively call eval dispatch.

  • So I'd better save the thing I'm going to need later, which

  • is the environment.

  • This was the result of recursively

  • calling eval dispatch.

  • It was evaluating the symbol f in that case.

  • Then it came back from eval dispatch, restored the

  • environment.

  • But in fact, the actual thing it ended up doing in the

  • evaluation is not going to hurt the environment at all.

  • So there's no reason to be saving the environment and

  • restoring the environment here.

  • Similarly, here I'm saving the argument list. That's a piece

  • of the argument evaluation loop, saving the argument

  • list, and here you restore it.

  • But the actual thing that you ended up doing didn't trash

  • the argument list. So there was no reason to save it.

  • So another way to say, another way to say that is that the,

  • the evaluator has to be maximally pessimistic, because

  • as far from its point of view it's just going off to

  • evaluate something.

  • So it better save what it's going to need later.

  • But once you've done the analysis, the compiler is in a

  • position to say, well, what actually did I need to save?

  • And doesn't need to do any-- it doesn't need to be as

  • careful as the evaluator, because it knows what it

  • actually needs.

  • Well, in any case, if we do that and eliminate all those

  • redundant saves and restores, then we can

  • get it down to this.

  • And you see there are actually only three instructions that

  • we actually need, down from the initial 11 or so, or the

  • initial 20 or so in the original one.

  • And that's just saying, of those register operations,

  • which ones did we actually need?

  • Let me just sort of summarize that in another way, just to

  • show you in a little better picture.

  • Here's a picture of starting--

  • This is looking at all the saves and restores.

  • So here's the expression, f of x, and then this traces

  • through, on the bottom here, the various places in the

  • evaluator that were passed when the evaluation happened.

  • And then here, here you see arrows.

  • Arrow down means register saved.

  • So the first thing that happened is the

  • environment got saved.

  • And over here, the environment got restored.

  • And these-- so there are all the pairs of stack operations.

  • Now, if you go ahead and say, well, let's remember that we

  • don't--that unev, for instance, is a completely

  • useless register.

  • And if we use the constant structure of the code, well,

  • we don't need, we don't need to save unev. We don't need

  • unev at all.

  • And then, depending on how we set up the discipline of

  • the--of calling other things that apply, we may or may not

  • need to save continue.

  • That's the first step I did.

  • And then we can look and see what's actually, what's

  • actually needed.

  • See, we don't-- didn't really need to save env or

  • cross-evaluating f, because it wouldn't, it

  • wouldn't trash it.

  • So if we take advantage of that, and see the evaluation

  • of f here, doesn't really need to worry about, about hurting

  • env. And similarly, the evaluation of x here, when the

  • evaluator did that it said, oh, I'd better preserve the

  • function register around that, because I might need it later.

  • And I better preserve the argument list.

  • Whereas the compiler is now in a position to know, well, we

  • didn't really need to save-- to do

  • those saves and restores.

  • So in fact, all of the stack operations done by the

  • evaluator turned out to be unnecessary or overly

  • pessimistic.

  • And the compiler is in a position to know that.

  • Well that's the basic idea.

  • We take the evaluator, we eliminate the things that you

  • don't need, that in some sense have nothing to do with the

  • compiler at all, just the evaluator, and then you see

  • which stack operations are unnecessary.

  • That's the basic structure of the compiler that's described

  • in the book.

  • Let me just show you how that examples a

  • little bit too simple.

  • To see how you, how you actually save a lot, let's

  • look at a little bit more complicated expression.

  • F of G of X and 1.

  • And I'm not going to go through all the code.

  • There's a, there's a fair pile of it.

  • I think there are, there are something like 16 pairs of

  • register saves and restores as the evaluator

  • walks through that.

  • Here's a diagram of them.

  • Let's see.

  • You see what's going on.

  • You start out by--the evaluator says, oh, I'm about

  • to do an application.

  • I'll preserve the environment.

  • I'll restore it here.

  • Then I'm about to do the first operand.

  • Here it recursively goes to the evaluator.

  • The evaluator says, oh, this is an application, I'll save

  • the environment, do the operator of that combination,

  • restore it here.

  • This save--this restore matches that save. And so on.

  • There's unev here, which turns out to be completely

  • unnecessary, continues getting bumped around here.

  • The function register is getting, getting saved across

  • the first operands, across the operands.

  • All sorts of things are going on.

  • But if you say, well, what of those really were the business

  • of the compiler as opposed to the evaluator, you get rid of

  • a whole bunch.

  • And then on top of that, if you say things like, the

  • evaluation of F doesn't hurt the environment register, or

  • simply looking up the symbol X, you don't have to protect

  • the function register against that.

  • So you come down to just a couple of, a

  • couple of pairs here.

  • And still, you can do a little better.

  • Look what's going on here with the environment register.

  • The environment register comes along and says, oh, here's a

  • combination.

  • This evaluator, by the way, doesn't know anything about G.

  • So here it says, so it says, I'd better save the

  • environment register, because evaluating G might be some

  • arbitrary piece of code that would trash it, and I'm going

  • to need it later, after this argument, for

  • doing the second argument.

  • So that's why this one didn't go away, because the compiler

  • made no assumptions about what G would do.

  • On the other hand, if you look at what the second argument

  • is, that's just looking up one.

  • That doesn't need this environment register.

  • So there's no reason to save it.

  • So in fact, you can get rid of that one, too.

  • And from this whole pile of, of register operations, if you

  • simply do a little bit of reasoning like that, you get

  • down to, I think, just two pairs of saves and restores.

  • And those, in fact, could go away further if you, if you

  • knew something about G.

  • So again, the general idea is that the reason the compiler

  • can be better is that the interpreter doesn't know what

  • it's about to encounter.

  • It has to be maximally pessimistic in saving things

  • to protect itself.

  • The compiler only has to deal with what

  • actually had to be saved.

  • And there are two reasons that something might

  • not have to be saved.

  • One is that what you're protecting it against, in

  • fact, didn't trash the register, like it was just a

  • variable look-up.

  • And the other one is, that the thing that you were saving it

  • for might turn out not to actually need it.

  • So those are the two basic pieces of knowledge that the

  • compiler can take advantage of in making

  • the code more efficient.

  • Let's break for questions.

  • AUDIENCE: You kept saying that the uneval register, unev

  • register didn't need to be used at all.

  • Does that mean that you could just map a

  • six-register machine?

  • Or is that, in this particular example, it

  • didn't need to be used?

  • PROFESSOR: For the compiler, you could generate code for

  • the six-register, five, right?

  • Because that exp goes away also.

  • Assuming--yeah, you can get rid of both exp and unev,

  • because, see, those are data structures of the evaluator.

  • Those are all things that would be constants from the

  • point of view of the compiler.

  • The only thing is this particular compiler is set up

  • so that interpreted code and compiled code can coexist.

  • So the way to think about it is, is maybe you build a chip

  • which is the evaluator, and what the compiler might do is

  • generate code for that chip.

  • It just wouldn't use two of the registers.

  • All right, let's take a break.

  • [MUSIC PLAYING]

  • We just looked at what the compiler is supposed to do.

  • Now let's very briefly look at how, how this gets

  • accomplished.

  • And I'm going to give no details.

  • There's, there's a giant pile of code in the book that gives

  • all the details.

  • But what I want to do is just show you the, the

  • essential idea here.

  • Worry about the details some other time.

  • Let's imagine that we're compiling an expression that

  • looks like there's some operator, and

  • there are two arguments.

  • Now, the--

  • what's the code that the compiler should generate?

  • Well, first of all, it should recursively go off and compile

  • the operator.

  • So it says, I'll compile the operator.

  • And where I'm going to need that is to be in the function

  • register, eventually.

  • So I'll compile some instructions that will compile

  • the operator and end up with the result in

  • the function register.

  • The next thing it's going to do, another piece is to say,

  • well, I have to compile the first argument.

  • So it calls itself recursively.

  • And let's say the result will go into val.

  • And then what it's going to need to do is start setting up

  • the argument list. So it'll say, assign to argl cons of

  • fetch-- so it generates this literal instruction--

  • fetch of val onto empty list.

  • However, it might have to work--

  • when it gets here, it's going to need the environment.

  • It's going to need whatever environment was here in order

  • to do this evaluation of the first argument.

  • So it has to ensure that the compilation of this operand,

  • or it has to protect the function register against

  • whatever might happen in the compilation of this operand.

  • So it puts a note here and says, oh, this piece should be

  • done preserving the environment register.

  • Similarly, here, after it gets done compiling the first

  • operand, it's going to say, I better compile--

  • I'm going to need to know the environment

  • for the second operand.

  • So it puts a little note here, saying, yeah, this is also

  • done preserving env. Now it goes on and says, well, the

  • next chunk of code is the one that's going to compile the

  • second argument.

  • And let's say it'll compile it with a targeted to

  • val, as they say.

  • And then it'll generate the literal instruction, building

  • up the argument list. So it'll say, assign to argl cons of

  • the new value it just got onto the old argument list.

  • However, in order to have the old argument list, it better

  • have arranged that the argument list didn't get

  • trashed by whatever happened in here.

  • So it puts a little note here and says, oh, this has to be

  • done preserving argl.

  • Now it's got the argument list set up.

  • And it's all ready to go to apply dispatch.

  • It generates this literal instruction.

  • Because now it's got the arguments in argl and the

  • operator in fun, but wait, it's only got the operator in

  • fun if it had ensured that this block of code didn't

  • trash what was in the function register.

  • So it puts a little note here and says, oh, yes, all this

  • stuff here had better be done preserving

  • the function register.

  • So that's the little--so when it starts ticking--so

  • basically, what the compiler does is append a whole bunch

  • of code sequences.

  • See, what it's got in it is little primitive pieces of

  • things, like how to look up a symbol, how to do a

  • conditional.

  • Those are all little pieces of things.

  • And then it appends them together in this sort of

  • discipline.

  • So the basic means of combining things is to append

  • two code sequences.

  • That's what's going on here.

  • And it's a little bit tricky.

  • The idea is that it appends two code sequences, taking

  • care to preserve a register.

  • So the actual append operation looks like this.

  • What it wants to do is say, if--

  • here's what it means to append two code sequences.

  • So if sequence one needs register--

  • I should change this.

  • Append sequence one to sequence two,

  • preserving some register.

  • Let me say, and.

  • So it's clear that sequence one comes first.

  • So if sequence two needs the register and sequence one

  • modifies the register, then the instructions that the

  • compiler spits out are, save the register.

  • Here's the code.

  • You generate this code.

  • Save the register, and then you put out the recursively

  • compiled stuff for sequence one.

  • And then you restore the register.

  • And then you put out the recursively compiled stuff for

  • sequence two.

  • That's in the case where you need to do it.

  • Sequence two actually needs the register, and sequence one

  • actually clobbers it.

  • So that's sort of if.

  • Otherwise, all you spit out is sequence one followed by

  • sequence two.

  • So that's the basic operation for sticking together these

  • bits of code fragments, these bits of

  • instructions into a sequence.

  • And you see, from this point of view, the difference

  • between the interpreter and the compiler, in some sense,

  • is that where the compiler has these preserving notes, and

  • says, maybe I'll actually generate the saves and

  • restores and maybe I won't, the interpreter being

  • maximally pessimistic always has a save and restore here.

  • That's the essential difference.

  • Well, in order to do this, of course, the compiler needs

  • some theory of what code sequences need

  • and modifier registers.

  • So the tiny little fragments that you put in, like the

  • basic primitive code fragments, say, what are the

  • operations that you do when you look up a variable?

  • What are the sequence of things that you do when you

  • compile a constant or apply a function?

  • Those have little notations in there about what they need and

  • what they modify.

  • So the bottom-level data structures--

  • Well, I'll say this.

  • A code sequence to the compiler looks like this.

  • It has the actual sequence of instructions.

  • And then, along with it, there's the set

  • of registers modified.

  • And then there's the set of registers needed.

  • So that's the information the compiler has that it draws on

  • in order to be able to do this operation.

  • And where do those come from?

  • Well, those come from, you might expect, for the very

  • primitive ones, we're going to put them in by hand.

  • And then, when we combine two sequences, we'll figure out

  • what these things should be.

  • So for example, a very primitive one, let's see.

  • How about doing a register assignment.

  • So a primitive sequence might say, oh, it's code fragment.

  • Its code instruction is assigned to R1, fetch of R2.

  • So this is an example.

  • That might be an example of a sequence of instructions.

  • And along with that, it'll say, oh, what I need to

  • remember is that that modifies R1, and then it needs R2.

  • So when you're first building this compiler, you put in

  • little fragments of stuff like that.

  • And now, when it combines two sequences, if I'm going to

  • combine, let's say, sequence one, that modifies a bunch of

  • registers M1, and needs a bunch of registers N1.

  • And I'm going to combine that with sequence two.

  • That modifies a bunch of registers M2, and needs a

  • bunch of registers N2.

  • Then, well, we can reason it out.

  • The new code fragment, sequence one, and--

  • followed by sequence two, well,

  • what's it going to modify?

  • The things that it will modify are the things that are

  • modified either by sequence one or sequence two.

  • So the union of these two sets are what

  • the new thing modifies.

  • And then you say, well, what is this--what registers is it

  • going to need?

  • It's going to need the things that are, first of all, needed

  • by sequence one.

  • So what it needs is sequence one.

  • And then, well, not quite all of the ones that are needed by

  • sequence one.

  • What it needs are the ones that are needed by sequence

  • two that have not been set up by sequence one.

  • So it's sort of the union of the things that sequence two

  • needs minus the ones that sequence one modifies.

  • Because it worries about setting them up.

  • So there's the basic structure of the compiler.

  • The way you do register optimizations is you have some

  • strategies for what needs to be preserved.

  • That depends on a data structure.

  • Well, it depends on the operation of what it means to

  • put things together.

  • Preserving something, that depends on knowing what

  • registers are needed and modified

  • by these code fragments.

  • That depends on having little data structures, which say, a

  • code sequence is the actual instructions, what they modify

  • and what they need.

  • That comes from, at the primitive

  • level, building it in.

  • At the primitive level, it's going to be completely obvious

  • what something needs and modifies.

  • Plus, this particular way that says, when I build up bigger

  • ones, here's how I generate the new set of registers

  • modified and the new set of registers needed.

  • And that's the whole--

  • well, I shouldn't say that's the whole thing.

  • That's the whole thing except for about 30 pages of details

  • in the book.

  • But it is a perfectly usable rudimentary compiler.

  • Let me kind of show you what it does.

  • Suppose we start out with recursive factorial.

  • And these slides are going to be much too small to read.

  • I just want to flash through the code and show you about

  • how much it is.

  • That starts out with--here's a first block of it, where it

  • compiles a procedure entry and does a bunch of assignments.

  • And this thing is basically up through the part where it sets

  • up to do the predicate and test whether

  • the predicate's true.

  • The second part is what results from--

  • in the recursive call to fact of n minus one.

  • And this last part is coming back from that and then taking

  • care of the constant case.

  • So that's about how much code it

  • would produce for factorial.

  • We could make this compiler much, much better, of course.

  • The main way we could make it better is to allow the

  • compiler to make any assumptions at all about what

  • happens when you call a procedure.

  • So this compiler, for instance, doesn't even know,

  • say, that multiplication is something that

  • could be coded in line.

  • Instead, it sets up this whole mechanism.

  • It goes to apply-dispatch.

  • That's a tremendous waste, because what you do every time

  • you go to apply-dispatch is you have to concept this

  • argument list, because it's a very general thing

  • you're going to.

  • In any real compiler, of course, you're going to have

  • registers for holding arguments.

  • And you're going to start preserving and saving the way

  • you use those registers similar to the

  • same strategy here.

  • So that's probably the very main way that this particular

  • compiler in the book could be fixed.

  • There are other things like looking up variable values and

  • making more efficient primitive operations and all

  • sorts of things.

  • Essentially, a good Lisp compiler can absorb an

  • arbitrary amount of effort.

  • And probably one of the reasons that Lisp is slow with

  • compared to languages like FORTRAN is that, if you look

  • over history at the amount of effort that's gone into

  • building Lisp compilers, it's nowhere near the amount of

  • effort that's gone into FORTRAN compilers.

  • And maybe that's something that will change over the next

  • couple of years.

  • OK, let's break.

  • Questions?

  • AUDIENCE: One of the very first classes--

  • I don't know if it was during class or after class- you

  • showed me the, say, addition has a primitive that we don't

  • see, and-percent add or something like that.

  • Is that because, if you're doing inline code you'd want

  • to just do it for two operators, operands?

  • But if you had more operands, you'd want to

  • do something special?

  • PROFESSOR: Yeah, you're looking in the actual scheme

  • implementation.

  • There's a plus, and a plus is some operator.

  • And then if you go look inside the code for plus, you see

  • something called--

  • I forget-- and-percent plus or something like that.

  • And what's going on there is that particular kind of

  • optimization.

  • Because, see, general plus takes an

  • arbitrary number of arguments.

  • So the most general plus says, oh, if I have an argument

  • list, I'd better cons it up in some list and then figure out

  • how many there were or something like that.

  • That's terribly inefficient, especially since most of the

  • time you're probably adding two numbers.

  • You don't want to really have to cons this argument list. So

  • what you'd like to do is build the code for plus with a bunch

  • of entries.

  • So most of what it's doing is the same.

  • However, there might be a special entry that you'd go to

  • if you knew there were only two arguments.

  • And those you'll put in registers.

  • They won't be in an argument list and you won't have to

  • [UNINTELLIGIBLE].

  • That's how a lot of these things work.

  • OK, let's take a break.

  • [MUSIC PLAYING]

[MUSIC PLAYING]

字幕與單字

單字即點即查 點擊單字可以查詢單字解釋

B1 中級

播放10A:彙編 (Lecture 10A: Compilation)

  • 2 0
    林宜悉 發佈於 2021 年 01 月 14 日
影片單字