Computer Architecture Arm Substitles3

Download as pdf or txt
Download as pdf or txt
You are on page 1of 29

Computer Architecture Essesntials by

ARM
Module 1, Intro
[music] I'm Richard Grisenthwaite, Chief Architect at Arm. What does a Chief
Architect do? The Chief Architect is responsible for the evolution of the Arm
architecture. One of the things about an architecture is, you want your
software to continue running on future products. So it's a very long term
commitment, and the role of the chief architect is essentially to curate the
architecture over time, adding new features as needed by the partner. Why are
microprocessors so important and ubiquitous? I think the fundamental reason is
that they are a general purpose device. They describe a basic instruction set
that does the stuff that you can construct pretty much any program out of.
Indeed, they're Turing complete, which means, by definition, you can create any
computable problem, solving a computable problem with the processor. They're
not customized to any particular usage, but they do two things really rather
well, which are data processing and decision making. In other words, having,
for example, added two numbers and compared them, you're then taking a branch
and making a decision based off that. Now in reality, an awful lot of general
purpose problems are ones that involve: work out some value or some criteria,
compare against that, and then make a decision on it. And essentially that
allows you to solve a huge number of different problems and that's why the
microprocessor has become so ubiquitous. What is the difference between
architecture and microarchitecture? Architecture is what it has to do, and
microarchitecture is how it does it. So for example, we will define in the
architecture a set of instructions that do "add" or "load" or "branch", but it
says nothing about whether you've got a ten-stage pipeline or a three-stage
pipeline. It says nothing about branch prediction. All of those sort of
features which actually determine the performance of the device; all of those
are part of the microarchitecture. The key point is that software that is
written to be compliant on the architecture will actually run on lots of
different implementations, lots of different microarchitectures that
essentially implement that architecture in different ways, choosing a trade-off
between the power, the performance and the area of the design. What does it
take to build a commercially successful processor today? If you actually start
from scratch with an established architecture, but we want to create a new
microarchitecture, we reckon, for a high end processor, you're talking about
three-to-four hundred person-years' worth of work in order to create a properly
competitive multi-issue out-of-order machine compared with the state-of-the-art
that you can get from Arm. In addition to that — and that's just to come up
with the RTL — in addition to that, you've then got to do the implementation.
If you're going to build that on a three-nanometer process, the leading edge to
get the best possible performance, you're talking about tens of millions of
dollars for the mask sets. There's a whole bunch of software you've got to go
and build on top of that and do all of that work. When we've looked at
companies that are interested in actually starting up, taking an Arm
architecture license — say I want to go and build my own business — we reckon
that you need to be prepared to spend getting on for half a billion dollars
before you're actually going to be successful because it takes time. Your first
product is not necessarily going to be fully competitive because it would be
slightly surprising if the first thing that you built was as good as what
people have been building for many years. It takes time to build up that
expertise and skills. And so you're going to see a couple of iterations before
even the best teams end up really being competitive. And so as I say, I think
if you went from nothing and wanted to essentially create something, and that's
using the Arm architecture with all of its existing software and so on, you're
talking the best part of half a billion. What is the Arm business model? The
Arm business model fundamentally is the licensing of semiconductor IP to as
wide a range of companies as possible and in as many ways as possible, in order
to maximize the uptake of the IP. When we talk about IP, we're talking about
essentially designs for processors and we license that both as an architecture
license, where you effectively gain the right to build your own processor to
the specification of the Arm architecture, or an implementation license where
we are licensing, actually, an implementation that is compliant with the Arm
architecture in the form of some richer transfer-level RTL. What makes Arm
different from its competitors? So if you go back to where we were when Arms
started out in the early 90s, there were many, many different architectures
available and they were all kind of doing the same thing but slightly
differently. They would have different instructions, different instruction
sets, and so software needed to be ported. A huge part of Arm's success
actually comes from the fact that we created a business model of licensing the
IP to make it very easy for people to build processors, to incorporate the
designs into their SoCs, into their systems. And that then made it very
straightforward for people to be able to use the Arm architecture. Now what
this then meant was, people said: I will port more software to this because
it's more widely available and you get this positive feed-forward effect
whereby more availability of hardware encourages more software, encourages more
hardware, and so on. And essentially that meant that a lot of people said:
there's no point in me having a different architecture. I'm not getting a
valuable difference from doing that. All I've got is, kind of, a needless
change to the software that I need to make. So actually the whole model, we
went on at Arm, which was: let's license our IP to a great number of different
players to come up with different solutions to meet the form factor of a camera
or a PDA, or whatever it was back in the day. Those things made it much more
straightforward for people to incorporate our technology. [music]

Module 1, Video 1
Computer architecture is the study of tools and techniques that help us to
design computers. More precisely, it helps us understand how to meet the needs
of particular markets and applications, using the technology and components
that are available. For example, we might need to produce the chip at the heart
of a smartphone using 10 billion transistors and a power of less than two
Watts. How do we achieve the performance a customer wants? The challenge is a
fascinating one, and one that requires a broad understanding. For example, what
target market are we designing for? What are the characteristics of the
applications to be run? How will programming languages and compilers interact
with the microprocessor? How best to craft the narrow interface between the
hardware and software? How to organize the components of our microprocessor?
And how to design a circuit, given the characteristics of individual
transistors and wires? Like many design problems, computer architecture
requires many trade-offs to be made and evaluated. Each design decision will
impact trade-offs between size, performance, power, security, complexity, and
cost. Trade-offs must be re-evaluated regularly, due to advances in fabrication
technology, applications, and computer architecture. Computer architecture must
be grounded in quantitative techniques and experimentation, but the endless
number of possible designs means that the field depends on a high degree of
human ingenuity and art. Perhaps surprisingly, the earliest computers and
today's most advanced machines have much in common. They both execute a stored
program constructed from machine instructions. These instructions perform
simple operations such as adding two numbers. Nevertheless, greater numbers of
faster transistors, and the application of a relatively small number of
computer architecture concepts, have enabled us to construct machines that can
perform billions of instructions per second, and shrink these machines to fit
in hand-held battery-powered devices. It is this rapid progress that has
supported breakthroughs in machine learning, drug discovery, climate modeling,
and supports our modern world where computation and storage are almost free.
The task of designing a microprocessor is split into different levels of
abstraction: "Architecture;" "Microarchitecture;" and "Implementation."
"Architecture" focuses on the contract between programmers and hardware. It
allows compatible families of microprocessor products to be built. The ARMv8-A
architecture is an example of this. Architecture includes the "Instruction Set
Architecture," or ISA, which defines what instructions exist. It also defines
precisely the behavior of memory and other features needed to build a complete
processor. "Microarchitecture" focuses on the organization and structure of the
major components of a microprocessor. It has to match the rules set by the
architecture. The microarchitecture still has flexibility though; and so the
implementation specifies the circuit detail precisely. This culminates in the
exact circuit design for manufacture. Each of these levels is vital, and each
comes with its own challenges and opportunities.

Module 1, Video 2
At the beginning of the 20th century, "computers" were people employed to
perform calculations. These computers used mechanical calculators to help them
perform arithmetic. They followed instructions to decide what calculation to
perform next. These instructions defined the algorithm or program they were
executing. They would consult paper records to find the inputs to their
calculations, and would store their intermediate results on paper so they could
refer back to them later. A modern electronic computer is organized in a
similar way. We will look in more detail at these components of a
microprocessor in the next video, but for now let's look at how it operates.
Microprocessors also follow instructions one by one, and then perform relevant
calculations. This idea of fetching instructions from memory and executing them
is called the "Fetch-Execute Cycle." In the "Fetch" stage, the computer reads
the next instruction from the program. This instruction is encoded in binary as
ones and zeroes, so it must be decoded to understand what it means. This is
done in the "Decode" stage. Once it is clear what to do, we move to the
"Execute" phase. This can involve different tasks such as reading memory,
performing a calculation, and storing a result. Once done, the computer is then
ready to begin the cycle again, by fetching the next instruction. Instructions
are normally fetched sequentially in order, but some special instructions
called "branches" can change which instruction will be executed next. For
branches, a calculation determines the next instruction. This can mean
evaluating a condition, or reading a register to determine the next
instruction's location. Branches allow computers to make decisions, and to re-
use instruction sequences for common tasks. A modern computer program, like a
web browser, contains millions of instructions, and computers execute billions
of instructions per second, but they all conceptually follow this "Fetch-
Execute Cycle."

Module 1, Video 3
Modern microprocessors are circuits built using anywhere from 1,000 to 100
billion tiny transistors. The key to designing circuits with such huge numbers
of parts is to build them from re-usable blocks. Let's take a look at some of
these. Each transistor is an electrically-controlled switch. When there is too
little voltage at the gate, the switch is off, so the electrical signal cannot
propagate from the drain to the source. However, when there is sufficient
voltage at the gate, the switch is on, so the signal does propagate. When
designing processors, we use digital electronics. The only voltage or current
values we consider represent zero and one, enabling us to build robust circuits
from imprecise components, even in the event of electrical noise or
manufacturing imperfections. This means our circuits represent binary numbers,
since they only have two states: zero and one. We use transistors to build
increasingly complex circuits. We can design circuits that can remember binary
values, or select between multiple inputs, or even do arithmetic, like
addition. We can then use those circuits as building blocks in even larger
designs. When designing a digital system, we must keep everything synchronized
to control when its behavior occurs. To do this, we use a "Clock Signal," which
is a wire in the circuit whose signal cycles between zero and one. We measure
the rate in Hertz. For example, if you hear a processor has a clock speed of
two Gigahertz, it means 2 billion ones and zeroes per second. The maximum speed
of the clock signal is determined by the longest, and therefore slowest, path
in the circuit between 2 clocked flip-flops. This is referred to as the
"Critical Path." The signal must have time to propagate all the way along the
critical path before the clock cycle completes. A microprocessor needs to do
many types of arithmetic. For this, we build an "Arithmetic Logic Unit" or
"ALU." This circuit receives 2 numbers as input, as well as an indication of
what operation to perform, for example, addition or subtraction. In addition to
logic, a microprocessor needs memory. Memory is organized as arrays of memory
cells that are able to store many "Words" of data. A specific word, commonly 32
bits in length, can be accessed by specifying its "Address." Each address is a
number that indicates the location in the memory that should be read or
written. Memory cells range from hundreds of bits to millions of bits in size,
but larger ones are slower to access, as signals and their long internal wires
take longer to propagate. For that reason, almost all microprocessors include
at least two types for storing data: a big slow "Data Memory," and a small fast
memory called a "Register File." In reality, the "Data Memory" may be
implemented using many different sizes of memory, as we'll see in Module 4. As
well as storing data in memory, we also use some memory to store the
instructions. We need a way to keep track of which instruction we will fetch
next, so we have a "Program Counter." This stores the address in Instruction
Memory of the next instruction to be accessed. Since instructions are encoded
in binary, we also have "Instruction Decode Logic" that converts that binary to
the various signals needed to control the microprocessor.

Module 1, Video 4
We've already seen how a microprocessor is controlled by instructions. But what
are they really? An instruction is a simple command that the microprocessor
hardware can perform directly. We write them as text like this to make them
easier to read. But for the microprocessor we encode them in binary. We use a
program called an assembler to translate between the human text and the binary.
In this video we'll be looking specifically at an Arm instruction set called
A64 but other instruction sets follow similar principles. Arithmetic and logic
instructions are the simplest type of instruction. The first word tells us what
operation will be performed such as addition or multiplication. The values
after this tell the processor where to put the result and where to get the
inputs. Values starting with X are addresses in the register file. Arithmetic
instructions read one or two registers and then put the result into a third
register. Branch instructions are used to make decisions and to repeat
instructions. Normally the microprocessor executes instructions in sequential
order but branches change that, and explicitly tell the microprocessor the
address of the instruction to run next. This is done by giving the address of
the next instruction in the instruction memory. Some branches are
unconditional, meaning they always occur and always affect the next instruction
address. Other branches are conditional, meaning the processor will perform a
calculation to decide whether to follow the branch or to continue executing
instructions sequentially following the branch. These are preceded by
comparison instruction to calculate the condition. Loads and stores are the
instructions for accessing the data memory. Loads copy values from memory to
the register file. Stores do the opposite. In both cases, the instruction needs
to know the address in the data memory and the location in the register file to
copy between. For data memory, loads and stores read an address from a base
register. They can also optionally add to this base address by reading another
register, or by simply specifying a number in the instruction itself. Using
sequences of instructions we can build programs. Here is an example of a small
program that implements Euclid's greatest common divisor algorithm. Let's take
a look at it working, one instruction at a time. To start with, the input
stored in X1 and X2 are compared. If they're equal, because we have found the
greatest common divisor a conditional branch instruction moves to instruction
7. If they're not equal, another conditional branch instruction can be used to
determine whether X1 is smaller. Finally, we use an arithmetic instruction to
subtract either X1 or X2 depending on which was larger and then unconditionally
branch back to the start. Here we can see an example of the program running.
One instruction executes at a time. As we step through the program, the values
in the registers X1 and X2 are shown. And we see these are modified each time
we reach instruction 3 or 5. The instructions repeat in a loop, as is common
for many different types of program. Each time round the loop instruction 0 and
1 are checking whether or not to continue the loop. When they detect X1 and X2
have become equal, the program finishes and the processor moves on to the next
task.

Module 1, Lab
In this exercise, we're going to be using ASim. ASim is a behavioral simulator
for a subset of the Arm AArch64 instruction set. Behavioral simulator means
that it's not simulating real circuit level details of a microprocessor it's
just simulating the behavior of each instruction as it runs. Nevertheless,
behavioral simulators are vital tools for computer architects. We use them to
check the designs we've built, match with the behavior we intended and we can
also use them to explore new ideas, for example adding new instructions to the
instruction set. We're just going to be using it though to get familiar with
the basics of Arm AArch64. So we can create a new file which will allow us to
type in some Arm AArch64 instructions and when we press assemble that will
cause ASim to begin the simulation of the instructions. If we scroll down we
can see that below we've put a little cheat sheet of Arm AArch64 instructions
that you can use as a reference. Do note that these are just examples though.
So for example this first one is mov X1 X2 and the descriptive text says that
this copies the value of register X2 to register X1. But equally that would
imply that if we use mov X3, X4 instead that would copy the value of X4 to the
register X3. So we don't have to just use these exact instructions, we can
tweak them as we need to, to achieve our goals. So going back to the ASim,
let's say that we wanted to add the number two to the number three. In our
cheat sheet we could see that there is an 'add' instruction but it requires
that the inputs for the addition are stored in registers. So first of all,
we're going to need to load up two registers with the numbers that we want to
add in this case two and three. So looking at our cheat sheet again we can see
that there's an instruction mov that allows us to do that. So if I write mov
X2, #3 this would cause the register X2 to be loaded with the value 3 and
similarly I could write mov X1,#2 and this would cause the register X2 X1 to be
loaded with the value two. Last but not least, we could then do the addition we
wanted to do, such as add X3 X1 X2. This will cause X1 to be added to X2 and
the results stored in X3. If I press assemble now, we will see that the UI
changes slightly. Here we can now see the simulated state of the processor.
ASim is only going to, because it's a behavioral simulator it's only going to
show the state before and after each instruction. And so right now we are
before executing the first instruction. That's the yellow highlighting there
and we can also see it in the simulated memory. If I press step we can now see
that that instruction has completed, and it's before executing the mov X1 X2.
And notably we can see that X2 has been loaded with the number 3 which is
exactly what we would expect. If I press step again, we can see that mov X1 X2
has now occurred. MOV X1 #2, sorry has now occurred which has loaded the value
2 into the register X one. And last but not least, if we step again we see that
X3 has become equal to five which is exactly what we would expect if registry
X1 was added to registry X2. So this allows us to get a feel for what it's like
for a machine to execute these instructions. We can reset back to the beginning
if we want to watch it go through again. If we want to do an edit for example
adding a new instruction we can just do that. Press assemble and that will add
the new instruction to our program, and we can see its effects by simulating.
Now in the exercise you will be invited to take an existing program and add 1
new instruction to it at the indicated position. Do note that when you assemble
the exercise program, you'll be taken to a gray bit of code, which is our
testing code. But as you step, you'll see that the simulator flicks between the
testing code and the code that we wrote the exercise one code. You can also
click between them using this. Feel free to try to work out what our testing
code does but you can just ignore it if you want to. The point of the exercise
is just to add the instruction in the indicated position. When you're
comfortable you've got the right instruction you can press run to get all the
way to the end. And if you really think that's correct if you scroll down to
the bottom of the page you'll see the submit button which you can use to send
in your answer. Best of luck.

Module 2, Intro
[music] Hello, my name is Martin Weidmann. I'm an engineer and product manager
with Arm's Architecture and Technology Group. I look after the A-profile
architecture and I maintain Arm's Interrupt Controller specifications. Computer
architecture is sometimes called a science of trade-offs. Why is everything a
trade-off when it comes to designing a processor? Let's take an example of
where we've had to make trade-offs when developing process architecture. So the
Arm architecture has a feature called Pointer Authentication. This is often
abbreviated to PAC for Pointer Authentication Code. What this feature is trying
to do is protect against a form of attack called ROP and JOP. These are Return
Orientated and Jump Orientated Programming, and it's where an attacker tries to
subvert things like the call stack to run legitimate code, but in ways that
weren't expected by the programmer or the compiler. PAC or Pointer
Authentication tries to defend against this kind of attack by using part of an
address to provide an encrypted signature. So we can check the signature and
the address match and if they don't, we can spot an attack in progress. So why
is this a trade-off? Well, because to add security, we want that signature to
be as big as possible. The bigger the signature, the more bits we use for that,
the stronger cryptographically that signature is. The trade-off is: the more
bits we use for the signature, the fewer bits we have available for other
things, such as the address. So you can have a big signature with a small
address, but if you want the address to get bigger, then you get a smaller
signature, and that's then cryptographically weaker. So the trade-off we have
to make when designing a technology like that is: What's the right amount of
bits for the signature? What's the strength of cryptography we need from that
signature in order to get the design goal, which is to defeat these attacks and
give us more robust computing? What sort of guiding principles do you use when
designing a processor? So when you're designing a processor, the key thing you
have to bear in mind is: What's it going to be used for? What you don't want to
end up with is a very expensive paperweight. So we need to understand the
requirements that the processor has to meet. We have to understand the design
trade-offs we're making and how they work into meeting those requirements. We
also have to consider not just the processor itself, but how we're going to
show that that processor is correct. We'll put as much time, if not more, into
testing and validating the design as we do into designing it. How do you design
a new microprocessor? If you wanted to create a new processor from scratch, the
first thing you're going to have to do is understand the market that that
processor is going to address and to then build a team to design that
processor. There isn't such a thing as one processor for every possible market.
The requirements for something like an embedded microcontroller are going to be
very different to what you want from the processor in your mobile phone, your
laptop, or your server. So you need to understand those requirements as the
first step into building a new processor. What determines the best design for a
microprocessor? So when you're designing a processor, you need to work out what
the best design for a given market or application is going to be. There's no
magic formula for this. It's going to depend a lot on what you're trying to
achieve with that processor. You need to understand things like the power
requirements, the performance requirements. Is it going to work in a highly
noisy electrical environment? There's a big difference between the reliability
requirements you need from something like a watch versus a satellite. So you
would take those requirements and you'd work out what the best set of trade-
offs is going to be, and that's an art more than it is a science. How do the
underlying technologies contribute to this best design? A lot of technologies
go into our processor. There's the design of the microarchitecture, the
implementation of the processor. There's the silicon process you're going to
use, how you're going to integrate that processor into an SoC or ASIC. Is it a
single die, or is it going to be using chiplets or multiple sockets? All of
those different technologies are going to be factors into how you design the
process or what trade-offs you make, and what performance and power you get out
of the design once you're done. In reality there may be many different 'best'
designs, so how do you pick one? So when you're designing a processor, what you
want is the best design. But often there isn't "a" best design, there's just
different trade-offs. You have to decide what the best set of trade-offs is for
the particular use case you're going for. And that's also going to depend on:
Is this a device that will be off the shelf, used for lots of different
applications—a general purpose processor? Or is this being designed for one
specific use case? Again, there isn't really a magic bullet or single answer
for this type of question. You need to understand how the processor is going to
be used and then use your experience to judge the trade-offs, and what will
give you the best mix of power, performance, area, cost, reliability for your
target use case.

Module 2, Video 1
[music] In this module, we're going to explore how to improve the simple
microprocessor design from Module 1 in order to allow it to execute programs
more efficiently. First, let's find out how long a program takes to execute.
The time taken to perform the average instruction is equal to the number of
clock cycles taken to perform an instruction multiplied by the duration of one
clock cycle. The time taken to run our program is found by multiplying the
average time to perform an instruction by the number of instructions in our
program. How could we make this faster? One thing we could try is to reduce the
number of instructions in a program. We might be able to optimize the code
removing unnecessary and repeated work and selecting instructions to minimize
code size and maximize performance. We could give our microprocessor the
ability to perform more operations in order to help programmers or compilers
further reduce the number of instructions in their program. For example,
allowing the loading of two data values at the same time might allow fewer
instructions to be used in the program. The downside to this approach, is that
adding more instructions will require extra circuitry in the processor and
therefore we likely increase the clock period. If the extra instructions are
rarely used this could even mean an overall decrease in performance. We see
this theme often in computer architecture trade-offs that we have to carefully
balance. Another approach is to use faster transistors perhaps constructed from
a more recent fabrication technology. This would reduce the clock period but
may increase costs. The rest of this module focuses on an optimization to
reduce the clock period called pipelining. This is the most important
optimization we use when designing processors. It uses a similar concept to an
assembly line in a factory where work can start on the next item before the
previous one finishes. Let's take a closer look. Imagine that each instruction
has to go through four circuits in a processor. If we attempt to do all of
these in one clock cycle this means our clock period is the latency of all four
circuits added together. If we were to pipeline this, we would add a pipeline
register in the middle. This divides the circuit into two sections called
stages. Notice that although each instruction takes a similar amount of time to
travel down the whole pipeline, the pipeline design can execute nearly twice as
many instructions per second. The throughput has doubled. This is because we
can set the clock period much shorter. It's now the maximum latency of the two
stages. We can pipeline into many stages and this allows for much faster
execution of programs. Unfortunately, though, pipelining a real microprocessor
design is not quite as simple because the processor has various feedback
signals and loops in the circuit. In the next video, we'll take a look at the
challenges of pipelining in practice. [music]

Module 2, Video 2
[music] In this video, we're going to look at applying the pipeline
optimization to a realistic microprocessor design. In the first module, we met
the components of a microprocessor, so let's look at how these are really
connected. This diagram shows all the connections needed for a real,
unpipelined microprocessor. Each clock cycle, the processor starts by fetching
an instruction from the instruction memory. Once the instruction reaches the
decode logic, it is decoded to produce the control signals necessary to execute
it. The exact control signals vary depending on the type of instruction. For
example, arithmetic instructions access the register file and interact with the
ALU. Ultimately, no matter how the instruction was executed, the last step of
each clock cycle is to update the program counter. This is done by the branch
unit. For non-branch instructions, this just means incrementing the program
counter. However, for branch instructions, the branch unit has to do some
calculations. When we apply our pipeline optimization to this design, we face
some challenges. The design has several loops because instructions have
dependencies. How can we break these cycles? The key observation is that not
every instruction is the same. In real programs, branch instructions usually
make up less than 20 percent of the program. For non-branches, the branch unit
doesn't actually need to wait for the ALU before calculating the result. Let's
look at how we can use this fact to pipeline the processor. Once the first
instruction shown in yellow reaches the pipeline register, we're ready to begin
fetching the next instruction, shown in blue. The yellow instruction can be in
the execute stage whilst the blue instruction is being fetched. Once the yellow
instruction is finished, the blue instruction is ready to enter the execute
stage and a new green instruction enters the fetch stage. What about the
branches though? Let's imagine this next yellow instruction is a branch. The
fetch stage works normally until the branch unit, but the branch unit cannot
proceed. Consequently, the pipeline stalls. The fetch stage spends a cycle
waiting whilst the execute stage executes the branch. Finally, once the ALU is
done, the branch unit can proceed and the next instruction, in this case blue,
can be fetched. Overall, this means that the processor wasted one cycle
stalling due to the branch. Since only 20 percent of instructions are branches,
this means that each instruction would require on average 1.2 cycles. The same
idea of stalling the pipeline can be used to create even longer pipeline
designs. This diagram shows a typical five-stage processor pipeline. In the
next video, we'll look at how we can manage or prevent some of the stalls in a
design like this. [music]

Module 2, Video 3
[music] Instructions within a program may be dependent on each other. That is,
one instruction may produce a value that a subsequent instruction consumes.
Data values may be communicated through registers or memory. The simple program
shown has a number of so-called true data dependencies. This means we must take
care to execute these instructions in order, and make sure results are
correctly communicated. Additionally, the outcomes of branch instructions may
affect the path taken through the program, and consequently, this affects
whether an instruction is actually executed. This sort of dependency is known
as a control dependency. In the previous video, we met a realistic processor
pipeline with five stages. Circumstances that prevent an instruction making
progress in our pipeline are known as pipeline hazards. Let's take a look at
how dependencies cause hazards. This program has a true data dependency. The
first instruction writes to register one, which is then read by the second
instruction. If we send this down our pipeline, we see that the second
instruction must stall, waiting for register one to be written, before it can
read and proceed. This is a data hazard. Unfortunately, dependent instructions
are common and stalling in this way would significantly increase the average
cycles per instruction. Let's take a closer look at the hazard though. The ADD
instruction is in the execute stage, meaning its result is being computed. The
SUB instruction needs that result to proceed. Rather than waiting for the ADD
to reach the writeback stage, we could add an extra path into our pipeline to
carry the output of one stage to a later instruction, making the result
available straight away. We call this a forwarding path. In this case, the ALU
result is forwarded to the SUB instruction to be used as X1. This small piece
of extra circuitry allows this data hazard to be eliminated completely.
Unfortunately, even if we add forwarding paths everywhere, it's not possible to
eliminate all data hazards. For example, this program has a data hazard due to
the load instruction. There are other types of hazard too. This program
contains a control hazard. We cannot be sure which instruction to fetch until
after the branch instruction executes. Consequently, this program has two stall
cycles. We will look in more detail at how control hazards can be mitigated in
the next module. Another class of hazards, called "structural hazards" occur
when two instructions require the same resources simultaneously. For example,
if instructions and data were stored in the same memory, and this could only be
accessed once per cycle, we would have to very frequently stall our pipeline to
let these stages access memory one-by-one. [music]

Module 2, Video 4
[music] In the previous videos, we explored how pipelining could improve
performance by reducing our clock period and by overlapping the execution of
different instructions. We also saw that it was sometimes necessary to stall
our pipeline to ensure that instructions were executed correctly. Ideally, our
average cycles per instruction, or CPI, will remain at 1.0. If we must stall,
however, this will increase. For example, if 20 percent of our instructions
were loads and each of these caused one stall cycle, our CPI would be 1.2. If a
further 20 percent of instructions were branches, and each of these caused two
stall cycles, our CPI would be 1.6. The longer we make our pipeline, the more
stall cycles there will be, and eventually the cost of stalls may outweigh the
benefit of the faster clock period. For example, let's imagine we added a stage
to our five-stage pipeline from the previous video. Now the number of stalls
after a branch instruction increases to three, hurting our CPI. On the other
hand, our clock period would improve. So whether or not this helps speed
program execution would depend on the exact details. It may eventually become
more difficult to reduce our clock period by adding further pipelining stages.
This is because it becomes harder to perfectly balance the logic between stages
and because of the constant delays associated with clocking and our pipeline
registers. To mitigate these issues, we will need to invest in more transistors
and our design will require more area and power. The deeper our pipeline gets,
the greater the investment we need to make in terms of area and power for the
same incremental improvement. Commercial processes today have anywhere from two
to twenty pipeline stages. The faster, more expensive and power-hungry
processors tend to have longer pipelines than the smaller, cheaper processes in
embedded devices. As with many techniques in computer architecture, eventually
it becomes more profitable to invest our time and resources in an alternative
way of improving performance. In later modules, we'll explore how we can reduce
the CPI, even in heavily pipelined processes. [music]

Module 2, Lab
[music] In this exercise, we're going to be using a model of a processor
pipeline to explore the effect of the pipelining optimization. Computer
architects use models like this to make high level decisions early on about
what parameters they will use for a processor and using a model such as this
saves the burden of actually building the processor to find out its
performance. The model is not simulating accurately the performance of the
processor but rather it's giving us an idea for what performance we might
expect. So what can we do with this model? Well, we can configure the number of
pipeline stages, which we can see affects the diagram. And we can also turn on
or off the forwarding optimization. As we change these numbers notice that the
design parameters change down here. So for example, the clock frequency is
improved by increasing the number of pipeline stages but the design area will
get bigger. And so this may be a consideration depending on the problem. We can
also choose which of two programs we're going to put down our pipeline. When we
press the step forward button the pipeline advances to the next clock cycle and
we can see the instructions have started to flow down our pipeline and
interesting events such as forwarding will be noted in the simulation.
Sometimes the simulation will detect that there will be a stall for example, in
this case, we can see that there is a data hazard because the instruction in
the red memory stage writes to register X13 which is read by the instruction in
the yellow decode stage and therefore a stall cycle is necessary in order to
allow the result to be generated. If we press the play button, the simulation
will proceed automatically and we can see various stall events happening as the
simulation proceeds. But notice that the the program we're simulating is nearly
1,000,000 cycles long so watching it play out at this speed is going to take
quite a while. So we can use the fast forward slider to simulate much, much
faster. Notice that the statistics down the bottom have updated depending on
the results of the simulation, and at this point we can see that the program is
finished and the simulation of the program, the simulated program took 3.98
milliseconds to execute. We can also see that just below, the results of past
simulations are stored in little tables so we can easily refer back to them
when we're doing later experiments. So as an experiment, let's imagine what
would happen if we disabled the forwarding optimization but change nothing else
and we'll just run this program through. What we can see immediately is the
design side is slightly better which is what we would expect. It's got 1%
better in fact in this case because of the lack of the forwarding wires. But
now that the program is finished, we can see that the program execution time is
a lot worse. 6.34 milliseconds is about 50% worse. So again, looking in our
table, we can compare the execution times in the area and we can see that in
most cases the forwarding optimization would be a big optimization here because
at the cost of an increase in the area of about 1%, we've had a improvement in
execution time of about 50% which is likely to be a good trade off, but not
always. It would depend on the exact scenario. Perhaps that 1% area is more
important than the performance of this program. In the exercise, you'll be
invited to design a or suggest a design using the number of pipeline stages and
whether forwarding is enabled that will meet certain criteria. You can play
about and do as many simulations as you wish to figure out what the best
program might be. Once you've got it set up select the processor that you're
happy with at the top and then scroll down to the submit button and press that.
Good luck. [music]

Module 3, Intro
[music] Hi, I'm Nigel Stevens. I'm Lead Instruction Set Architect for the Arm
A-Profile architecture. I've been at Arm for about 14 years and I have
responsibility for the Arm V8-A instruction set including recent developments
such as the Scalable Vector Extension and Scalable Matrix Extension. What do we
mean by the instruction set architecture? The instructions in architecture,
primarily, most people think of I guess as the OP codes, the encodings of
instructions that are executed by an Arm-based processor. But it also includes
other aspects as well such as the exception model, system programming features,
memory management and suchlike. The architecture for Arm is rather distinct
from what other companies may call an architecture. For Arm, architecture is a
legal contract, if you will, between hardware and software. If software uses
only those instructional codes and features of the ISA that are described by
the Arm architecture to perform its work, and the hardware that it's running on
implements all of those op codes and features exactly as defined by the
architecture, then any architecturally compliant software will run on any
architecturally compliant hardware that implements that Arm architecture. And
that doesn't mean just processors from Arm itself, but also processors that are
designed by our partners and which we have validated are conformant with our
architecture. How do you decide which instructions to include in an ISA? When
we are looking at requests from partners or from internal research to add a new
instruction, we go through quite a long process of trying to justify that
instruction, or, quite commonly, a set of instructions rather than a single
instruction. We have to show that it gives us some real benefit in performance,
the performance of your code running on that CPU. Or maybe not performance.
Maybe it's security you're trying to achieve. But it has to give you some
really concrete benefit that is worth the cost of adding all of the software,
the validation software, the implementation costs for all of the different
implementations, compiler support, so on and so forth. It has to meet the... It
has to answer that cost benefit analysis. What is the difference between an ISA
and a microarchitecture? The difference between an instruction set
architecture, or ISA, and the microarchitecture is that the ISA is an abstract
concept. It defines a set of instruction encodings which software can use, and
which hardware has to recognize and implement. How that is implemented is a
choice for the microarchitecture. So the instruction set architecture is fixed,
it's defined by Arm. The microarchitecture is defined by whatever team of
people is designing that CPU. And there are many different approaches to
implementing the Arm architecture, from very small, efficient cores with in-
order pipelines up to very high-performance, state-of-the-art, out-of-order
execution, and everywhere in between. So the microarchitecture is
implementation-specific, the architecture is generic, and software written for
the architecture should run on any microarchitecture. Why does Arm produce
processors with different instruction sets? Arm supports multiple instruction
sets. Some of that is to do with legacy: you can't abandon your legacy
software, your legacy ecosystem. So as the architecture has advanced and we've
introduced major new instruction sets, we still have to continue to support old
software. It takes years, maybe 10 years to move the software ecosystem to a
major new ISA. So for example, AArch64, which is the 64-bit architecture that
we introduced with Arm V8, also supported the AArch, what we called AArch32,
the old 32-bit architecture that was implemented in the Arm V7 architecture,
and prior to that including the Arm and the Thumb instruction sets. And we
needed to do that because, while some software might start to migrate to the
64-bit architecture, there's still a lot of software on the planet which is
going to continue using the 32-bit architecture, and that has to survive. So
that's part of the reason: it's about legacy. You can't just obsolete the whole
world when you introduce a new architecture, a new instruction set architecture
in particular. There are other reasons as well, which is there are certain
instruction sets that are different for reasons of the ecosystem that they're
working with. So if you were to compare, for example, the A-profile
architecture that's designed for application processors that run rich operating
systems with virtual memory supporting SMP, symmetric multi processing
operating systems running large applications, whatever it may be: web browsers
on your phone or something, or a web server in a server farm somewhere. You
have your R-profile architecture, which is designed for high-performance, real-
time embedded systems. The constraints there are somewhat different. The
instruction set is actually fairly similar to the A-profile, but some of the
underpinnings of the architecture, the system side of the architecture, are
simplified in order to give more consistent and predictable real-time response
to things like interrupts or memory translation and suchlike for real-time
systems. And then at the other extreme you have the M-profile architecture
which is designed to be capable of being built in a very simple, ultra-low
power implementation with low code size and again, similar to the R profile,
very predictable real-time performance. So the answer is there are different
instruction sets for reasons of the market that they're trying to address, and
then there are different instruction sets because, well, we have history.
[music]

Module 3, Video 1
[music] In the previous module, we explored how pipelining can be used to
improve performance. We also saw how it is sometimes necessary to stall our
pipeline to ensure our program is executed correctly. In a simple pipeline, it
will be necessary to stall the pipeline whenever we encounter a branch
instruction. This is because we must wait until our branch is executed before
we can be sure which instruction to fetch next. As a recap, branches are
instructions that change which instruction in the program will be executed
next. There are two types of branches: conditional branches and unconditional
branches. Unconditional branches always change which instruction executes next,
whereas conditional ones may or may not, depending on the computations in the
program. In real programs, between approximately one fifth and one quarter of
all instructions are branches, and the majority of these are conditional.
Executing a branch involves calculating the new address to load into our
program counter. This is the branch's "target address." However, conditional
branches have an extra task: we must first determine whether the branch is
taken. If the branch is not taken, we can effectively ignore the branch and
fetch the next instruction as normal. Recall the processor performance equation
from an earlier video. Since we have to wait for branches to complete before
fetching the next instruction, we generate stall cycles. These increase the
average number of "cycles per instruction," which reduces our microprocessor's
performance. The longer our pipeline gets, the longer it is before each branch
is resolved, and the more costly branches become. Can you think of a way to
avoid some of this stalling? One idea is to evaluate branches earlier in the
pipeline, for example in the Decode stage instead of in the Execute stage. This
can indeed help to reduce the number of stalls, but we may still need to stall
if the branch depends on other instructions that haven't been executed yet.
Another idea is to continue fetching instructions in program order, effectively
assuming that each branch is not taken. The number of stalls in the pipeline
for a not-taken branch is zero in this design. On the other hand, if the branch
is in fact taken, the subsequent instructions that we fetched will be
incorrect. So, we must remove all instructions that have been fetched on this
incorrect path from our pipeline. This is called "flushing" the pipeline.
Unfortunately, in real programs, branches are taken much more than not taken.
Could we then simply assume instead that all branches will be taken? Sadly not,
no, because then we would also need to know the specific "target address"
immediately, in order to know which instruction to fetch next. It may at first
seem impossible to know this before the instruction is decoded. However,
computer architects have found a way to do exactly this. The next video will
look at "dynamic branch prediction:" the idea of predicting the behavior of the
branch instruction before it has even been fetched. [music]

Module 3, Video 2
[music] In this video, we'll explore how to predict the behavior of a branch
instruction. This can sometimes eliminate the cost of branches altogether.
Fundamentally, a branch involves changing the value of the program counter,
which is the address of the next instruction in the program. If we could
predict what this change will be, quickly and accurately, we would have no need
to stall. Precisely, we need to predict that we are fetching a branch, predict
it as taken or not taken, and predict what its target address is. How could we
ever make such predictions? What would we base them on? Well, since
instructions are often executed multiple times, we can accurately make these
predictions based on just the instruction address. If we've previously seen
that a particular address contains a branch, and we see that address again, we
can predict whether that branch will be taken, and its target address, based on
its behavior last time. Amazingly, for real programs, simply predicting
repeating behavior is typically around 90 percent accurate. This means we could
eliminate stalls caused by branch instructions 90 percent of the time. Let's
apply these insights to try to build a branch predictor. We will add two extra
blocks to the Fetch stage of the pipeline we met in Module 2. The first will
remember information about recently executed branches. This will include the
program counter values of branches and their target addresses. This memory is
called the "Branch Target Buffer" or BTB. The second block will make
predictions about whether an address containing a branch is taken or not. We
simply call this the "branch predictor." In the next video, we will look at
these in more detail. Combining these two gives us all the information we need
to predict the next value of the program counter based solely on the current
value of the program counter. We don't even need to decode the instruction to
make this prediction. Here we can see how a running branch predictor behaves
for a sample program. Each program counter is checked in the BTB to see if it's
predicted to be a branch and to identify its predicted target. We also
simultaneously check the branch predictor to see if the branch is predicted to
be taken. Based on these predictions, the branch unit computes the predicted
next program counter. Many cycles after the prediction, feedback will be given
by the rest of the pipeline as to whether or not the prediction was correct.
Whenever the prediction is wrong, we have to flush the pipeline and update the
BTB and branch predictor. The pipeline will then resume fetching from the
correct program counter as computed by the pipeline. The majority of
instructions are not branches, so most of the time the branch unit just adds 1
to the program counter. The BTB contains the instruction address and target
address of some recently executed branches. The exact number of entries in the
BTB varies considerably. BTBs in large modern processors contain many thousands
of branches. In operation, the BTB checks the supply program counter against
its memory to see whether it has a match, and if so, it returns the target
address. Otherwise, it predicts that the instruction is not a branch. After
each branch executes, the BTB is updated with the true target address. BTB
cannot be arbitrarily large, so it may have to forget an existing branch to
remember a new one. A simple BTB design like this is typically around 90
percent accurate at predicting target addresses in real programs. [music]

Module 3, Video 3
[music] In the previous video, we met the two major components of dynamic
branch prediction, the BTB and the branch predictor. In this video, we'll take
a deeper look at the branch predictor. It predicts whether or not a branch will
be taken, based on the program counter. A simple branch predictor would try to
remember what the branch did last time and predict that the same behavior will
repeat. Let's see how such a predictor might be organized. Remembering a branch
prediction for every possible instruction address would take up far too much
memory, so we reuse the same memory for different branches via a process called
"hashing." We hash the address of the branch to a smaller number. This does
unfortunately lead to a problem called "aliasing," where two different branches
can hash to the same value, but this is rare in practice. Let's see what
happens now, when we execute a simple loop. We see a misprediction when it
encounters our branches for the first time, and another when we exit the loop.
The first case will be dependent on the value in our predictor's memory, and it
may be that we are able to predict the branch correctly the first time we see
it. The second case is hard to avoid, although some more sophisticated branch
predictors will learn how many iterations a loop will make. A common
improvement to this simple scheme is to avoid instantly flipping our prediction
just because the branch does something unexpected once. This can be achieved
with a saturating counter, which instead remembers how many times the branch
has been taken recently, versus not taken. The counter increments when the
branch is taken and decrements when not taken. It predicts "taken" if the
majority of recent executions of this branch were taken. When building higher-
performance processors, we often have to discard many instructions every time
we mispredict a branch, so accurate branch prediction is very important.
Therefore, branch prediction is still an active area of research. One of the
key ideas used is correlation between branches. In real programs, a common
pattern is for example a pair of branches that always have opposite behavior. A
branch predictor can take advantage of this by remembering a history of whether
or not recent branches were taken or not taken and incorporating this in the
hash. Another idea is to combine multiple different types of predictor in a
"tournament predictor." We use a "meta predictor" to predict which of two
branch predictors will do a better job. As you can see, branch prediction can
get quite complicated. [music]

Module 3, Video 4
[music] In this module, we've met the concept of branch prediction. Even using
relatively small simple circuits, we can accurately predict real branch
behavior more than 95 percent of the time. Could we do better, and do we really
need to? In small, simple processors, these prediction accuracies are fine,
because each misprediction causes only a few stall cycles. Increasing the
accuracy is not that impactful. However, in complex processors with very long
pipelines, the difference between 98 percent and 99 percent prediction accuracy
can be significant for performance as a misprediction could incur dozens of
stall cycles. Accurate prediction really does matter. One of the problems we
face in making accurate predictors is that they need to be small enough and
fast enough to fit in a microprocessor. We can imagine all sorts of ways to do
accurate branch prediction, but if their circuit would be slower than simply
executing the branch, they would not be useful. Modern high-performance
processors will often have multiple branch predictors, for example a small fast
one and a slower complex one. The slower one can override the prediction of the
fast one if it thinks it got it wrong, which does incur some stall cycles, but
fewer than a total misprediction. Another problem we face is that some branches
are just very hard to predict. No matter what technique is used, there are some
branches in real programs that are effectively "random". For example, when
compressing or decompressing data, the combination of the underlying algorithm
and the input data may provide no clear patterns to a prediction. No matter how
hard we try, some branches will never be predicted correctly 100 percent of the
time. A final problem is that since the predictors work based on observing the
program, there will always be a period of time when the predictors train on a
program to learn its behavior. [music]

Module 3, Lab
[music] In this exercise, we're going to be using a branch predictor simulator
to explore dynamic branch prediction. This simulator will accurately simulate
the details of a branch predictor but it uses a trace of a real program
executing on a real machine to avoid the need to simulate the rest of the
processor. Computer architects use techniques such as this to quickly explore
one area of process design understanding that perhaps the accuracy may not be
completely correct given that we're not simulating the full process of design.
The interface allows us to configure details of our branch predictor for
example the maximum of the saturating counters used in the branch predictors
table or the hash function. And we can see the impact of changes on the delay
of the branch predictor and also the design size which are two key metrics when
designing a branch predictor. Once we've happily configured the design we want
we can press run to simulate a program and the results of that program's
execution will be displayed in the rest of the stats. So for example, we can
see here that the predictor predicted 95.24% of the branches correctly. Which
is fairly good and this resulted in overall execution time of the program of
5.335 milliseconds. Just below we can see a table that records previous
simulation runs as well so we can do multiple experiments and see which
produces the best results. For example, if we were curious about the effects of
using a saturating counter with a maximum of three rather than one we could
change that and notice that the design size has substantially increased and
when we press run, we noticed that the predictor accuracy, however, has also
increased. It's gone up to 96.31% and consequently the execution time the
program has fallen slightly and so we can compare these two designs and see
whether or not this represents a good trade-off for our processor. Perhaps the
area cost is justified or perhaps it's too much. It would depend on the exact
processor we're trying to design In the problems you'll be invited in to come
up with designs that are suitable for particular constraints. For example,
particular constraints on the runtime of the program or the design size. Once
you've configured the branch predictor that you think meets the objectives you
can scroll down to the submit button and click that and you'll be told whether
the answer is great or not. Good luck. [music]

Module 4, Video 1
[music] So far, we've looked at the microprocessor's "datapath"— meaning its
execution units, registers, and control circuitry. We have given less attention
to its memory. We usually implement memory using a different type of chip than
the microprocessor, using a technology called DRAM. It is very dense, allowing
us to store lots of data in a small area. However, one issue with DRAM is its
speed. Since the 1980s, processor performance has increased very rapidly at
roughly 55 percent per year, so CPUs of today are many orders of magnitude
faster than those of 40 years ago. In contrast, memory performance has grown
much more modestly. Whilst memories are also much faster than they were in
previous decades, their performance has not kept pace with processors. This
leads to a processor-memory performance gap, with it becoming more costly to
access memory as time goes on. One of the issues that makes these memories slow
is their size. We can make a memory as fast as our microprocessor, if it is
very small. However, this is the opposite of what programmers want. Programmers
can use extra memory to solve more complex problems, or to solve existing
problems faster. This complicates microprocessor design because although we
want a large memory to hold all our data, large memories are slow and this
slows down our processor. How do we overcome this? What if we could get the
speed benefit of a small memory alongside the size benefit of a large memory?
One solution is to have a large, slow memory and a small, fast memory in our
system. For this to be useful, we need the small memory to hold the data that
we use most often, so that we often get the speed benefits of accessing it, and
only rarely have to access the slow memory. Whilst there are different
arrangements of these two memories, the configuration that is most often used
is an "on-chip cache memory." The small memory sits inside the processor
between the pipeline and the large memory. We keep all data in the large main
memory, and put copies of often-used data in the small memory, which we call a
cache. But we are not limited to only one cache! Our pipeline reads memory in
two places: when fetching the instructions; and when accessing the data. It
makes sense to have two caches here, each optimized for different purposes. The
instruction cache is optimized for fast reading of instructions at Fetch. The
data cache is optimized for reading and writing data from the memory stage. We
will often put a larger "level 2" cache between these two caches and the main
memory. The L2 cache is a "medium-sized" memory: faster and smaller than main
memory, but slower and larger than the two L1 caches. Using a hierarchy of
caches reduces the bandwidth requirements to main memory and the energy cost of
moving data around. [music]

Module 4, Video 2
[music] We previously looked at the need for a cache, which can be used to
store often-used data for fast access by the processor. But which data is used
often enough that it is worth including in the cache? Programs are mostly made
of loops. Here's a simple one that sums values in memory. It displays the two
characteristics that we can exploit to decide what data to put into a cache.
Let's look briefly at how it works. Each time round the loop, there are two
loads and one store. The first load is to load the data we're summing. The
other load, and the store, are to update the running sum. Notice two things
here. First, we access the running sum over and over again; each time round the
loop. Second, when we access part of the data in one loop iteration, we've
already accessed its predecessor in the previous iteration, and will access its
successor in the next. Caches exploit two types of "locality" in programs in
order to be effective. The first is temporal locality: if a piece of data is
accessed, it is likely that it will be accessed again in the near future. The
running sum has temporal locality. The second type of locality is spatial
locality: If a piece of data is accessed, then its close neighbors are quite
likely to be accessed in the near future. By close neighbors, we mean data
whose memory addresses are not far from each other. The data accesses have
spatial locality. It turns out that most programs exhibit lots of temporal and
spatial locality. We can exploit this to determine what to put in the cache.
Exploiting temporal locality is fairly easy: we simply see which values have
been accessed and place them in the cache. Exploiting spatial locality is also
quite simple: when a piece of data is accessed, place its neighbors in the
cache. We'll see in the next module how this is actually achieved. We use
locality to guess what values will be accessed next and store them in the
cache. If we are correct, we get the benefit of fast memory, but if we are
wrong, we must perform a slow access to main memory— just as we would if the
cache were not present. In real programs, we see hit rates of 90 percent or
more in microprocessor caches, resulting in vast performance improvements.
However, it's worth noting that some programs have less locality, and for those
programs, caches offer little performance benefit. [music]

Module 4, Video 3
[music] We've looked at the reasons why we build caches, but how do they
actually work? To the outside world, a cache simply takes an address as input,
and either provides the data that is stored at that location at output, or
returns a signal to say that it doesn't have it. If the data is found, this is
called a "cache hit". If the data is not in the cache, this is called a "cache
miss", and it means we must look for the data in main memory instead. After
each miss, we update the contents of the cache. The fundamental building block
of a cache is called the "cache line". It's a number of data bytes from
consecutive addresses in main memory. Cache lines are typically 32 or 64 bytes
long, and a cache typically has an array of hundreds to many thousands of
lines. The line captures spatial locality, because it is larger than the data
read by a single load instruction. When a cache miss occurs, the whole line
containing that data is copied to the cache from main memory, meaning we have
nearby values for future accesses. When a request comes in, we use some bits
from that address to index the line array. Just like in the last module on
branch prediction, this leads to the problem of aliasing again, since selecting
only some of the bits to index into the array is like a hash. This means that
many addresses map to the same line in the cache, but we can only store one of
their lines of data. We need to note down which addresses' line is currently
stored in the data array, in what we call the "tag array". There is one tag for
each cache line. When we access the cache, we access the tag array with the
same index to see if the data we need is present. This design is called a
"direct-mapped cache". Direct-mapped caches work fairly well, but for some
programs, we can be unlucky with the program accessing to aliasing lines
frequently. We can do something about this by duplicating both the arrays, so
that each line of data can now be stored in one of two places in the cache.
When we access the cache, we look at both arrays and only get a miss if neither
of the tags match. This is called a "2-way set-associative cache", because each
line has a set of two places or "ways" it could reside. The "associativity" of
this cache is therefore two. Set-associative caches introduce a further
complication: when we want to add data, where do we put it? In a 2-way set-
associative cache, there are two choices. How we decide which cache line to
evict is called the "replacement policy". There are many different types of
replacement policy. A simple one is just to make a pseudo-random choice from
all the possible cache lines. Another option is to keep track of when each
cache line was last accessed and to evict the one last used furthest in the
past. This is called a "least recently used policy", and takes advantage of
temporal locality. It does, however, means storing extra information in the
tags to track usage. Many other ideas are possible too. [music]

Module 4, Video 4
[music] Now that we've seen how caches work, let's see how they affect the
performance of a processor. Recall the processor performance equation, where
the processing time is proportional to the average cycles per instruction.
Without a data cache, if 20 percent of instructions are loads, and main memory
takes 20 cycles to access, our CPI figure must be at least 5. However, if we
provide a cache that holds the required data 80 percent of the time... ...and
only takes 2 cycles to access, our CPI reduces to 2.2, which is a significant
improvement! We can isolate the memory terms in this equation to get the
average memory access time —abbreviated to AMAT— which allows us to compare
different cache configurations more easily. Changing the cache configuration
will impact the AMAT. There are many different cache parameters we can change,
such as the size, replacement policy, associativity, whether we put data in the
cache for stores or just for loads, and so on. For example, reducing the size
of the cache will improve the access time for a hit, but will also increase the
miss rate. Let's say that we can halve the access time to 1 with a
corresponding halving of the hit rate. This alters the AMAT to 13, which in
this case is worse for performance overall. It's also useful to look at why an
address might miss in the cache. Broadly speaking, we can divide cache misses
into three different categories. Compulsory misses occur when we attempt to
access an address that we have never seen before and so never had the
opportunity to cache it. Capacity misses occur when there is more data being
accessed than the cache could hold, even if we had complete freedom in where to
put each cache block. Conflict misses occur in caches where there are more
addresses hashing to the same index than arrays to hold the data. We can alter
our cache configurations to lower these misses, but as always, there are trade-
offs involved. Compulsory misses can be reduced by increasing the cache block
size, to take advantage of spatial locality. But for a fixed cache size, this
reduces the number of different addresses or cache lines that can be stored. A
technique called "pre-fetching" can also be used to predict the addresses that
will soon be accessed, and bring their data into the cache early. But this
increases energy consumption, and may make the cache perform worse if the
predictions are not highly accurate. Capacity misses can be reduced through
increasing the size of the cache. Although, as we saw before, this impacts the
number of cycles taken to determine a hit. Conflict misses can be reduced
through increasing the number of cache blocks in each set, with an increase in
energy consumption as a side effect of this. [music]

Module 4, Lab
[music] In this exercise, we're going to be using a cache memory simulator to
explore the effects of cache memories on processor performance. The simulator
accurately simulates the behavior of the cache memory system, but it's using a
model of the rest of the processor to quickly allow us to simulate the effects
of the cache on the processor without needing to simulate the full processor.
We can configure a number of parameters about our cache, for example, the
number of levels in our cache, whether or not the cache separates instructions
and data, or is unified, keeping them both in the same cache. We can also
configure the size, the line size, and the associativity, and changing these
numbers will affect design parameters, such as the access times in the case of
an L1 level one cache hit or a cache miss, and also the design size. Once we're
happy we've found design we'd like to investigate, we can press "run", at which
point the simulator will run through the program. We can see the hit rates for
instructions in the level one cache, and also data in the level one cache are
displayed. And we can also see the average memory access time, the results from
this. And then below everything, we can see a table of past simulations so that
we can quickly refer back to our previous experiments when we do new ones. So,
for example, let's say we were curious about the effects of increasing the size
of the cache. If we change the parameter and then press "run", we can
immediately see that the design size has substantially increased, which sort of
makes sense because we've doubled the contents of the cache, the size of the
contents of the cache, and therefore we'd expect roughly double the area. And
we can also see that the hit rates have improved. So there's about a 1%
improvement to the L1 instruction cache hits and a 1% improvement to the L1
data cache hits, which has reduced the overall average memory access time. And
so we can compare these two designs to see which of them we think is better.
It's a trade-off of course, though. The larger design has got better
performance, but it is larger, and so depending on the context, we may need to
pick the smaller design or the bigger design depending on our performance
goals. In the exercises, you'll be invited to come up with a series of designs
for caches that meet certain performance goals. For example, you'll have a
constraint on the area of the design or the execution time of the program, and
you need to optimize the cache to meet those goals. Best of luck. [music]

Module 5, Video 1
[music] In this module, we'll look at how to further improve performance by
exploiting "instruction-level parallelism." In Module 2, we explored how
pipelining can improve the performance of our processor. This reduced our clock
period, and allowed execution of instructions to be overlapped, improving
throughput. One way to boost performance further would be to create a much
deeper pipeline. At some point, this would mean even the ALU in our Execute
stage will be pipelined. Consider the simple program shown in the slide. Some
instructions are dependent on a result from the previous instruction. Remember
in our 5-stage pipeline that these dependent instructions could be executed in
consecutive clock cycles with the aid of data forwarding. If execution takes
place over two pipeline stages within the pipeline, we need to stall if
adjacent instruction share a dependency. This allows time for the result to be
computed. The programmer may be able to rewrite their program to get the same
result with fewer stalls, by placing an independent instruction between our
pair of dependent instructions. In this case, we can move the third and fifth
instructions earlier to optimize performance. The performance of programs that
run on our "super-pipelined" processor would, to some degree, be determined by
the availability of independent instructions that could be executed in parallel
in the pipeline. This is "instruction-level parallelism"—or ILP. Very deep
pipelines are problematic as they would require: a very high-frequency clock to
be distributed across the chip very precisely; careful balancing of logic
between many, very short, pipeline stages; the pipelining of logic that is
difficult to divide further into stages; and the division of logic at points
requiring many pipelining registers to be inserted. A different approach to
exploit ILP is to make our pipeline wider rather than deeper. In this design,
the processor will fetch, decode, and potentially execute multiple instructions
each cycle. Such a design avoids the problems of a super-pipelined processor,
although as we'll see in the next video, it does introduce some new
complications. [music]

Module 5, Video 2
[music] In this video, we are going to explore "superscalar" processors, which
can process multiple instructions in each pipeline stage. In our simple 5-stage
pipeline, there is at most one instruction per pipeline stage. At best, we can
complete one instruction per cycle. We call such a design a "scalar" processor.
In a 2-way superscalar version of this processor, we would extend this design
so it is able to fetch, decode, execute and writeback up to two instructions at
a time. In general, superscalar processors may vary the number of instructions
that can be processed together in each stage. Let's step through the design.
Our instruction cache will need to supply two instructions per cycle. Typical
superscalar processors only ever fetch adjacent instructions on a given cycle.
This can lower performance if, for example, the first instruction fetched is a
taken branch, because then the second would not be required. Note that now
every cycle lost due to control hazards will cost us two instructions rather
than one, so accurate branch prediction matters even more in superscalar
designs. The Decode stage must now decode and read the registers for two
instructions simultaneously. Fortunately, we are able to extend the register
file design to read many register values at the same time. The Decode stage
also needs to check whether the two instructions are independent. If so, and if
the functional units they both need are available, it can "issue" them for
execution in parallel on the next clock cycle. Otherwise, in this simple
design, it will only issue the first, and keep the second back. A simple design
such as this—where two instructions are fetched, decoded and issued— is called
a "2-way" or "dual-issue" processor. In other designs, the width may vary at
different stages of the pipeline. To support the execution of multiple
instructions at the same time, the Execute stage is expanded and contains two
execution pipelines. It's common for these to have slightly different
capabilities to save area. For example, the top pipeline can execute both ALU
and memory instructions, while the second pipeline only executes ALU
instructions. To ensure that dependent instructions can execute on consecutive
clock cycles, we must add data forwarding paths. These data forwarding paths
must allow results stored in either execution pipeline to be forwarded to the
input of either ALU. During writeback, we need to store both results to the
register file. This means the register file must be redesigned to allow two
writes per clock cycle. Overall, these changes typically require 25 percent
more logic circuitry in our processor, compared with a scalar processor. But
we'd expect an improvement in execution time of between 25 and 30 percent for
real world programs. [music]

Module 5, Video 3
[music] We've seen that instruction-level parallelism can be used on
superscalar processors, to run them faster than would be possible on a scalar
processor. But how much of a speedup is this in practice? Ultimately, this
depends on how much instruction-level parallelism is possible in a typical
program. How might we measure this? We can do this initially without
considering any constraints that will be imposed by the processor it will run
on. Let's consider the instructions executed by the program. Let's assume that
we can predict all the branches in the program perfectly. Then we can ignore
branch instructions, as they don't need to flow down our pipeline. Now let's
imagine we can execute any instruction as soon as the data it needs is ready.
That is, we are only restricted by the presence of true data dependencies. Note
that some dependencies are carried through writes and reads to memory. Rather
than considering program order, we can now just look at the order the
dependencies impose on instructions. This is referred to as "data-flow
analysis." Assuming each instruction takes exactly one cycle to execute, the
fastest possible execution time of the whole program in cycles is given by the
longest path in the data-flow graph. The instruction-level parallelism of this
program is the number of instructions divided by this duration, as this gives
the average number of instructions we would need to be able to execute each
cycle to achieve this duration. In real programs, this can be anywhere from
around five, to hundreds or even thousands. An active area of research and
innovation for computer architects is to imagine processor designs that can
expose and exploit as much of this parallelism as possible. One insight
architects have had is that superscaler processors need to have a fast supply
of instructions to be able to analyze dependencies effectively. This often
means that the front end of our processor pipeline is much wider than the rest
of the pipeline, so that it can "run ahead" and see what behavior the program
will have next. Fast and accurate branch prediction is vital, as we often have
to predict multiple branches ahead accurately, to achieve good performance.
Another key insight is that we don't have to wait to execute the instructions
in program order. If all the dependencies of an instruction are satisfied, the
instruction can proceed down the pipeline even if previous instructions are yet
to execute. This can reduce program execution time by taking advantage of more
instruction-level parallelism. In practice though, this creates extra
complications, as we will see in the next module. [music]

Module 5, Lab
[music] In this exercise, we will be using a simulator to explore superscalar
microprocessor design. The simulator has a number of parameters that we can
configure, such as the number of pipeline stages, the width of the Fetch stage,
the width of the issue, and the number of ALUs in the design. We can see a
diagram of the processor that we've created and we can also see a number of
parameters about that design, for example the clock frequency and the overall
area of the design. When we press step, the simulator will advance one clock
cycle and so for example here we can see that the fetch stage has fetched the
first 4 instructions. However, immediately we see one of the problems with
designs such as this, which is the three of the four instructions that have
been fetched are in fact useless because the first instruction was an
unconditional taken branch and therefore the remaining three instructions will
not be executed by the program and so these will immediately be discarded on
the next cycle. Pressing the run button allows us to simulate and we can use
the fast-forward option to simulate much quicker in order to get to the end of
the long program execution. In this case we can see that our design achieved an
average cycles per instruction less than one, which is to say that we on
average executed more than one instruction per cycle, which means that our
superscaler design has fundamentally worked, and we can see that, for example,
the overall execution time of the program is 1.6 milliseconds. In a table
below, we can also see a record of our previous simulation runs. So let's say
for example, we were curious about the effect of increasing the issue width by
one. We can make that change and then press run again in order to run our new
design, and when it finishes we can scroll down to take a look and we can see
that the program execution time has indeed improved down to 1.51 milliseconds
at a cost of only 1% area. So it looks like this was a very good improvement to
our design and it's almost surely going to be a beneficial trade-off in
practice. In the exercise you will be invited to configure a number of
different superscalar processor designs with various targets in terms of clock
frequency, design area and execution time. Once you're happy that you've
configured the process that you think completes the exercise, you can scroll
all the way down to the bottom, where you'll see the submit button that you can
press to have your answer checked. Good luck. [music]

Module 6, Video 1
Introduction:

So hi, my name is Peter Greenhalgh. I'm Senior Vice President of Technology and
an Arm fellow. I'm responsible for the Central Technology Group at Arm. We're
about 250 people. We work on everything from machine learning to CPU, GPU,
system IP, and the solutions that we create as well. And we basically path-find
future technology at the product level that goes into all of our products and
the IP that we produce. Arm is known for the power efficiency of its
microprocessors. How have you managed to keep a focus on power when building
processors with very complex and power-hungry features? We've got some really
great design teams. In fact, we churn out I think more CPUs than pretty much
anyone else does on the planet. I think we're producing something like four or
five CPUs per year. So we've got a lot of experience in designing for power
efficiency and performance, and in fact we can leverage the understanding that
we have all the way down to microcontrollers through to the smaller A-class
processors, all the way up to the high performance. There's a lot of sharing
between the teams in terms of strong knowledge and capability, and insight into
how to design for both performance and power efficiency. More specifically, I
mean, ultimately, you have a performance goal that you need to achieve, and
then as part of that you have to figure out how to get the best possible power
out of the design when you're achieving that performance goal. And to do that,
there's kind of some different ways of looking at it. There's the really
detailed orientated work that you need to do around things, like clock gating,
data gating, all the things to try and stop unnecessary power use deep within
the microarchitecture when the instructions are flowing through the pipeline or
data's moving through the pipeline. And then there's essentially the structure
of the design that you've created. And that then dictates fundamentally what
the power of the design is going to be. You can't fix a design that's got bad
structure with improved clock gating, data gating, and just good low-level
design. You have to marry the two together. And that high-level work that you
do is around making sure that the pipeline is well balanced, that you aren't
opening up the pipeline, going too wide too soon; you're extracting data,
you're extracting information as late as you possibly can and just when you
need it, and not just pipelining it down through the design for the sake of it;
and then, fundamentally, good microarchitecture around branch prediction, which
stops you putting things down through the pipeline that you're just ultimately
going to flush; good pre-fetching on the data side so that you make sure you
get the data in the design when you need it, and you're not sitting around
waiting for it. So you have to marry that altogether, and we've got a lot of
great techniques in order to achieve that, which fundamentally, I say, you need
to achieve the performance target, and then everything else comes together to
achieve that performance target in the best possible energy efficiency. How did
Moore's Law affect computer architectures of the past, and what will its
influence be on future designs? Gordon Moore's influence on the industry has
been massive, and the tenets behind the law still continue today, albeit in a
slightly different form. I mean, I started designing at .18 Micron, essentially
180 nanometers, and here we are today working on 3 nanometers. So it's a vast
difference now compared to when I started 22 years ago. And there's no way we
could have got to where we are today without the process scaling from all of
the foundries out there and all the companies that provide the foundry
technology. So it's a little bit like magic, all of the work that they do. I
can't say I understand it in detail, but it's incredible technology, and that
allows us... If it hadn't happened, we'd still be stuck in all the designs
which were fairly simple. There's no way that we'd have got to the sort of
designs that we have today of massively out of order, deep, deep amount of
instruction and depth, very, very wide designs. All of that has been made
possible by the steady improvement, a predictable improvement of the foundries.
And that's kind of one of the key points which really was captured by Moore's
law or is captured by Moore's law of that kind of predictable knowledge that
you will get an improvement in the process. Is it 10%, is it 15? Is it 20% on,
say, power, for example? It kind of doesn't matter in a way because you can
work with what you eventually get. You can do things like voltage scaling to be
able to make use of the power that's available to you. Is it 5%? Is it 10% on
frequency? Again, it kind of doesn't matter in a way. But what matters is when
we start designing a processor today and we finish it in 18 months time, and
then two years after that it arrives in the product in the shops that consumers
can buy. We know that over that period, the process improvements have happened,
which allows us to liberate essentially more performance, more energy
efficiency from the design. And we don't mind too much if it takes another
three months or six months to get to the process. We don't mind too much if the
performance or power is not exactly where it was predicted at the start. But,
ultimately, we know we'll get an improvement, and we know there'll be an
improvement in two years, and three years, and four years, and Moore's Law may
have slowed, but it's certainly not stopped.

[music] As we saw in the last module, instruction level parallelism can be used
to improve program execution time in our microprocessor designs. To enable
this, the compiler creates an optimized instruction schedule when the program
is converted into machine code. Unfortunately, the compiler cannot know
precisely what will happen at run-time, so this design is constrained by the
order of instructions in the program. The compiler won't know what the
program's input data will be, whether branches will be mispredicted, or whether
memory accesses hit or miss in our data cache. In contrast, a superscalar
processor with "out-of-order" execution can produce an instruction schedule at
run-time, only constrained by true data dependencies and its hardware limits.
This schedule is produced on demand and so can even change each time the code
runs. To do this, we introduce an "issue window" or "issue queue" after the
Decode stage. This holds instructions until they can be executed, not
necessarily in the order they arrived in. Within this window, instructions can
be issued whenever their dependencies are available, and when a functional unit
is available to process it. To be able to detect when an instruction is ready
to be issued, we must know whether the instruction's dependencies are ready
when it enters the issue window. We must then update this status as new results
are produced. To implement this, the names of result registers of executed
instructions are broadcast to the issue window. The instructions waiting there
compare the register names to the registers they require. However, this scheme
has a problem: A register will be written multiple times in the program, and
since the instructions are executed out-of-order, the register name alone is
not sufficient to record dependencies. It also means that instructions would
have to wait until all previous reads of a register had finished before
executing. These are called "false dependencies." These problems can be
resolved by "renaming" register names at run-time so that each "in-flight"
instruction writes to a unique destination register. We use a "physical
register file" that is large enough to ensure we don't run out. We keep a
"register mapping table" to store the mapping between architectural, compiler-
assigned registers, and physical registers. Register reads to the same
architectural register are renamed consistently, so that dependencies can be
tracked correctly with physical register names. Physical registers are reused
only when they are no longer used by any instruction currently in-flight or any
entry in the register mapping table. The other big issue with out-of-order
execution is memory dependencies. Load and store instructions can have memory
dependencies because they access the same memory location. To detect this, we
need to compare the computed memory addresses that the instructions access. We
thus split memory operations into two steps: address calculator and memory
access. We issue their address calculation step as soon as the dependencies are
available. Then, the memory access step is placed in a special load-store queue
to be sent to our data cache as soon as possible. We carefully ensure that
operations that access the same address are kept properly ordered, but
independent accesses can be reordered if beneficial. No access can occur until
the addresses of all previous accesses are known. Since memory writes are
irreversible, store instructions must also wait until we are certain that they
will execute. [music]

Module 6, Video 2
[music] In the previous video, we outlined the concepts of out-of-order
execution, and register renaming. The issue window will be filled with
instructions fetched along the path that our branch predictor believes the
program will take. While we hope our branch predictor will be correct in most
cases, it will sometimes be wrong. How do we handle such cases? A simple
approach is to start by recording the original program order of the
instructions, and then to monitor their progress. We call the structure that
stores the instructions the "reorder buffer." As each instruction executes and
produces a result, we can mark it as done. When the oldest instruction has
completed, we can remove it from the end of the reorder buffer, and the
instruction is said to have "committed." This stream of committed instructions
represents how our program would be executed on a simple in-order pipeline or
by an unpipelined processor. It usefully also provides a point at which we can
process exceptions. For example, if the program divides by zero or attempts to
access memory that does not exist. We also check branch instructions as they
complete in order. If they have been mispredicted, we flush the reorder buffer,
our instruction window and any currently executing instructions and start
fetching down the correct path. To preserve correctness, we must also restore
our registers and the register map table to the values they had when we
mispredicted the branch. This can be done with the aid of a second register map
table, updated only when instructions commit in program order. This can simply
be copied to the map table used by our renaming hardware to "rewind time" for
the processor. All the register values we need will be present, as we don't
recycle registers before we know they will not be needed again. In reality,
handling branches in this way is too slow. Processors instead take many copies
of the register map tables and can handle branches as soon as they are
resolved, and we discover they have been mispredicted. They can also
selectively neutralize the in-flight instructions in the datapath that are on
the wrong path, rather than flushing all of these instructions away. [music]

Module 6, Video 3
[music] We can now bring everything together and look at what a typical
pipeline for an out-of-order superscalar processor might look like. The Fetch
stage is aided by an accurate branch predictor as we met in Module 3. It will
fetch a group of instructions on every clock cycle. This group of instructions
will be requested from the instruction cache, and will be from consecutive
memory locations. Branches may reduce the number of useful instructions that
can, in practice, be fetched in on each cycle. The Decode stage decodes
multiple instructions in parallel. At this point, modern high-performance
processors may also split complex instructions into simpler operations or
"macro-ops." In some cases, there may also be opportunities to combine simple
instructions into a single operation. The next step on an instruction's journey
is renaming to receive a unique destination register. As we saw in the last
video, this increases opportunities for out-of-order execution. Remember, there
are several times more physical registers in our processor than those available
to the compiler. Instructions are placed in the reorder buffer, and are also
"dispatched" to the Issue stage. They will wait in the window as necessary, and
are ready to be issued once all their operands are available. In the most
complex of today's superscalar processors, there may be hundreds of
instructions buffered in the issue window at the same time. Instructions
finally commit in program order. At this point, any physical registers that are
no longer needed can be added back to the pool of free registers. These are
then assigned during the register renaming step. Once an instruction is issued,
it reads its operands from the physical register file. The Execute stage
consists of many functional units operating in parallel. These may each support
different operations and take different numbers of cycles to execute. A network
of forwarding paths is also provided to ensure we can execute any dependent
instruction on the next clock cycle after the generation of the result. This
requires being able to quickly communicate—or "forward"— a result from the
output of any functional unit, to the input of any other. Some instructions
will need access to memory. After computing their addresses, they are placed in
the processor's load-store queues. "Stores" are sent to memory in program
order, but "loads" can often be sent out of order, and ahead of other older
stores or loads that are not yet ready to be issued to memory. The memory
system reduces the average memory access time by providing numerous levels of
cache memory. After generating results, we write them back to the register
file. This overview is representative of the fastest modern microprocessors
found today in laptops, smartphones and servers. Whilst much extra innovation
goes into real designs, they generally follow the ideas discussed in the
course. [music]

Module 6, Video 4
[music] One question computer architects always ask themselves is: "how much
can we scale up our design?" Let's take a look at some further potential
optimizations to our out-of-order superscalar processor. We could try to make
it wider. For example, by doubling the number of parallel instructions, we can
fetch, decode and execute more instructions per cycle. Would this double our
performance? Sadly, no, things are not that simple! In practice, some
components quickly become very complex, and performance gains may be hard to
extract. For example, today's largest machines fetch at most ten instructions
per cycle from their instruction caches. Fetching more instructions than this
offers minimal performance gain, despite a large hardware cost. If we increase
the number of registers, the size of our issue window, the size of our load-
store queues, or perhaps use a larger and more accurate branch predictor, our
processor's performance will only improve slightly despite a significant
increase in the size of these structures. After a point, the increase in
performance is no longer worth the cost of the extra transistors. It's also
possible that performance might reduce overall as we may need to lower our
clock frequency as the structures get larger. Finally, we could introduce more
pipeline stages, but we know this doesn't necessarily lead to higher
performance, as mispredictions may become more costly. The combination of these
issues means that extracting performance using instruction-level parallelism
alone becomes more expensive as more performance is sought. This graph shows
how the energy cost of executing an instruction grows quickly as we try to
build higher performance processors. Let's look at some example designs.
Suppose we have a core, which requires a certain area. If we double its area,
its performance improves, although there is a small rise in energy per
instruction. If we quadruple its area instead, its performance has now doubled
compared to our original core, while energy has increased by 50 percent. Going
further, if we increase our processor's area by a factor of 10, performance is
only 2 point 5 times our original core, but energy per instruction is now 3
times higher. Its performance does not improve as fast as the cost of running
it! Of course, engineers are clever and determined, and are constantly
developing new techniques to bypass many of these issues. This means the
performance of processors—even ones running a single thread or program— still
improves by around 10 to 25 percent each year. Nevertheless, ultimately we
often need more performance than can be provided by instruction-level
parallelism alone. A modern solution is to employ multiple processor cores on
the same chip—called a "multicore" processor. This changes the task for
programmers; they may need to redesign their programs to take advantage of such
parallelism, but if they can, it can give vast performance benefits. As we've
learned throughout the course, every decision involves trade-offs and
compromise. We are faced with a fascinating but often highly-constrained design
problem. We've seen how performance bottlenecks, that at first seem impassable,
can be overcome with innovative designs. What might the future hold for
microprocessors? Can you think of ideas? What would you design? [music]

Module 6, Lab
[music] In this exercise, we'll be using a simulator to explore an out-of-order
superscalar processor design. The simulator allows us to configure a number of
aspects of our processor, for example, the number of pipeline stages, the width
of the Fetch stage, the size of the issue window, the number of ALUs, and the
size of our re-order buffer. The changes will be reflected in the pipeline
diagram, which we can see below, and also in the statistics below that, with
key design metrics, such as the clock frequency, clock period, and design size,
visible below. When we press "step," the simulation will advance by 1 clock
cycle, and so we can see, for example, on the first clock cycle the 1st 4
instructions are loaded. Although three of them are unusable because they
follow a taken branch and therefore these will not be executed and will be
discarded on the next clock cycle. We can press "run" to watch our design in
action and in order to quickly get to the end, we can use the "fast forward"
feature to simulate the millions of instructions in this particular program.
After the simulation is complete, we can check below to see a number of
statistics about our pipeline, which are useful for understanding why the
performance is as it is, and in particular we can see the program execution
time. 1.18 milliseconds gives us the overall execution time of the program that
our design achieved, and notably our average instructions per cycle gives us
the number of instructions that we were able to complete on each clock cycle,
in this case well over 1, indicating that we are taking advantage of
instruction level parallelism in this simulation. Below all that, we can see
our completed simulations and we can check back on these as we explore new
designs. So let's say, for example, we were curious about the effect of
increasing the re-order buffer size. We could change that and then press "run"
to quickly run our next experiment. And then if we scroll down to our table, we
can compare the results and see that actually the program execution time was
substantially increased by increasing the size of the re-order buffer.
Although, admittedly, this did come at a not insignificant increase in the area
of our design, and so whether or not this represents a good trade-off in
practice would very much depend on the problem we're trying to solve. In the
exercises, you'll be given a number of scenarios for processors, which
generally revolve around certain constraints on the design size, or the clock
frequency, or the target program execution time. And once you think you've
found a design that meets the goals required, configure it up using the
settings and then scroll down to the "submit" button at the bottom, and click
that to have your answer checked. Best of luck. [music]

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy