You might be busy preparing for the finals. Take a break with free pizzas and have a good time with your friends and the professor. Also, there will be awards for excellent processor designs.
We use the following benchmarks for the official performance evaluation:
Q0. Please describe any feature(s) of your processor or implementation that would be considered novel, cool, unusual and/or different than the "average" 141L project. (I.e., any branch prediction/btb scheme, pipelining tricks, shadow bits, interesting instructions, use of special purpose registers, interesting addressing modes, banking, I/O devices, implementation of multicore, etc) This will help qualify your team for awards.
Q1. What is the maximum operating frequency of your processor? Fill out the following table. For the evaluation of fib_native and fib_sg, use input value 12 (the result must be 144). Explain how you acquired the number of cycles for execution and the dynamic instruction count.
| Frequency: | fib_sg | fib_native | sort_sg |
|---|---|---|---|
Q2. Fill out the following table on resource usage of your design. If there are any other resources we missed here, include them.
| Resource Type | Used | Available | Utilization |
|---|---|---|---|
| 4 input LUTs | |||
| I/O Blocks | |||
| Block RAMs |
Q3. Include the finalized datapath schematic of your processor and indicate the critical path on the schematic.
Q4. Include the final code for your SuperGarbage and fib_native benchmarks.
You can boost the performance of your processor by improving 1) cycle time, 2) CPI, and 3) instruction count.
Make sure to estimate the benefit of the optimization before you do it so that you do not waste time!
The cycle time of your processor not only depends on your design, but also on how Xilinx turns your design into hardware. Since the quality of the Xilinx tools' output depends on the optimization parameters you give to the Xilinx tools, you can try different options to get better results. First, you should give the tools an aggressive cycle time target to the Xilinx tools so that it will not give up early on optimizing your design. This can make a considerable difference in the resulting frequency, making your processor faster. There may be other options available in Xilinx tools (such as "effort levels") to explore. You may want to speak with your classmates to find out about anecdotal experiences with various parameters.
As you reach diminishing returns from tweaking Xilinx synthesis and implementation parameters, the next step is to try to improve the cycle time of your processor by modifying the design itself (i.e., by the datapath and control logic that you specified in verilog.) By examining the timing reports and other Xilinx outputs, you can determine the parts of your design (e.g., the critical path) that you may want to focus on optimizing. You often can rewrite the code to reduce critical paths (e.g., reordering muxes, or moving logic across pipeline stages, or pipelining more.) Make sure to keep a backup copy of your source (or if you are comfortable, use source control!) so that you can revert back to older, correct versions if you encounter bugs.
Although students' implementations will take a single cycle on most instructions, there may be a few opportunities to improve CPI. One area is the frontend of the processor (i.e. reduce misprediction rate and reduce misprediction penalty.) It might be possible to pipeline stores so that they only take one cycle (by not waiting for the store to finish before starting the next instruction), or to have loads execute in a pipelined fashion. Save your work if you try to make this change, as it is likely to require a fair amount of debugging.
The final factor that can affect the performance of your processor is the number of instructions to execute in a benchmark. You can reduce the number of instructions by carefully optimizing your assembler code, or using a technique like loop unrolling. For example, when you handle a big constant, you can use a load instruction instead of multiple load immediate instructions to save the instruction count. However, keep in mind that you need to examine your code carefully to see which techniques would have significant impact on the performance. You can optimize the code as much as you want, but you are not allowed to change the algorithm used in the benchmarks. For instance, optimized implementations of fib must be recursive and must do the same number of comparisons as the original version. You are not allowed to optimize the SuperGarbage binaries (but you are allowed to optimize your super garbage virtual machine.)
Q5. Describe the optimizations you have applied. Why did you choose them, and how effective were they in terms of reducing the execution time for each benchmark? If you did not have enough time to apply optimizations, you can skip this question.
Q6. If you have more time, what would you do to boost the performance of your processor? Why?
Recently, most mainstream processors consist of multiple cores on a chip to exploit the increasing number of available transistors on a chip. Surprisingly, transforming your processor design into a multi-core design is not that hard. See the following dual-core design example.
The dual-core example above consists of two slightly modified cores. The primary change for a dual-core system is in the location of the data memory. While, for a single core processor, the data memory is located inside the backend module, in a dual core system, it is located outside of a core at the top level. The two cores share one data memory module as shown in the figure above.
Sharing the data memory raises the issue of contention on the memory. Since the data memory module cannot handle two simultaneous memory requests from the both cores, it has to arbitrate requests. One simple solution is always prioritize the request from core 0 over that of core1. If a request cannot be served due to a contention, refused signal is asserted to the core whose request is rejected. However, this scheme is not fair; the execution of core 1 could be significantly delayed if the benchmark is memory-intensive. To address this problem, you might be able to devise an arbitration method which handles requests in a fair manner. (We sketched the logic for this on the board in class.)
More sophisticated designs may employ banking, where the data memory is actually composed of several smaller memories. The lower bits of the address indicate which bank an address goes to. In this case, the arbitration logic will only assert the refused signal if the two cores try to address the same bank.
How can your processor benefit from being a multi-core processor to execute provided benchmarks? In general, taking advantage of multicore requires that you parallelize your application. This means dividing it into two pieces that communicate so that the cores are each executing part of the computation at the same time.
Of the benchmarks used to benchmark your processor (fib_native, fib_sg, and sort_sg), fib_native is the easiest to parallelize. You should see almost linear speedup (i.e. N times faster with N processors), except for any slowdown associated with memory contention (to reduce memory contention you could bank the memory.) Note that for this purpose,
Here is some psuedo-code for parallelized-fib:
int sync = 0;
int val = 0x3DEADBEEF;
int fib_core0(int n)
{
if (n < 5)
return fib_internal(n);
val = n-2; // write value to core 1
sync = 1; // signal to core 1 that value is ready
int temp = fib_internal(n-1); // compute my part of fib
while (sync == 1); // wait for core1 to be ready
return temp + val;
}
int fib_core1(int n)
{
while (1) {
while (sync == 0); // wait for core0 to send value
int temp = fib_internal(val);
val = temp; // write value for core0 to receive
sync = 0; // signal back to core1 that we are done;
}
}
So, basically Core 0 will call fib_core0, and Core 1 will call fib_core1. Core 0 will transmit work to Core 1, and then start computing on its own. When Core 0 is done, it waits until Core 1 has finished, and then adds the result and returns.
SuperGarbage is a bit harder to parallelize. Probably not impossible, but difficult.
A few minor details for getting this to run. One thing to keep in mind is that the processors need to locate their stacks in different places, and that any private variables have to be located in different places so that they do not clobber each other. One think to keep in mind is that you will have two instruction files, and one data file. The instruction files need to have the same notion of where exactly any shared variables are located. One way to do this is to have only one program, and to load the instruction file into both instruction memories, and the data file into one of the memories. Then, when the processors come out of reset, they need to figure out which is the slave and which is the master, and then jump to different sections of code. Since the processors are identical, how do they differentiate? One way is to have a special I/O port that tells them which processor # they are.