rlm@421
|
1 %% This is an example first chapter. You should put chapter/appendix that you
|
rlm@421
|
2 %% write into a separate file, and add a line \include{yourfilename} to
|
rlm@421
|
3 %% main.tex, where `yourfilename.tex' is the name of the chapter/appendix file.
|
rlm@421
|
4 %% You can process specific files by typing their names in at the
|
rlm@421
|
5 %% \files=
|
rlm@421
|
6 %% prompt when you run the file main.tex through LaTeX.
|
rlm@421
|
7 \chapter{Introduction}
|
rlm@421
|
8
|
rlm@421
|
9 Micro-optimization is a technique to reduce the overall operation count of
|
rlm@421
|
10 floating point operations. In a standard floating point unit, floating
|
rlm@421
|
11 point operations are fairly high level, such as ``multiply'' and ``add'';
|
rlm@421
|
12 in a micro floating point unit ($\mu$FPU), these have been broken down into
|
rlm@421
|
13 their constituent low-level floating point operations on the mantissas and
|
rlm@421
|
14 exponents of the floating point numbers.
|
rlm@421
|
15
|
rlm@421
|
16 Chapter two describes the architecture of the $\mu$FPU unit, and the
|
rlm@421
|
17 motivations for the design decisions made.
|
rlm@421
|
18
|
rlm@421
|
19 Chapter three describes the design of the compiler, as well as how the
|
rlm@421
|
20 optimizations discussed in section~\ref{ch1:opts} were implemented.
|
rlm@421
|
21
|
rlm@421
|
22 Chapter four describes the purpose of test code that was compiled, and which
|
rlm@421
|
23 statistics were gathered by running it through the simulator. The purpose
|
rlm@421
|
24 is to measure what effect the micro-optimizations had, compared to
|
rlm@421
|
25 unoptimized code. Possible future expansions to the project are also
|
rlm@421
|
26 discussed.
|
rlm@421
|
27
|
rlm@421
|
28 \section{Motivations for micro-optimization}
|
rlm@421
|
29
|
rlm@421
|
30 The idea of micro-optimization is motivated by the recent trends in computer
|
rlm@421
|
31 architecture towards low-level parallelism and small, pipelineable
|
rlm@421
|
32 instruction sets \cite{patterson:risc,rad83}. By getting rid of more
|
rlm@421
|
33 complex instructions and concentrating on optimizing frequently used
|
rlm@421
|
34 instructions, substantial increases in performance were realized.
|
rlm@421
|
35
|
rlm@421
|
36 Another important motivation was the trend towards placing more of the
|
rlm@421
|
37 burden of performance on the compiler. Many of the new architectures depend
|
rlm@421
|
38 on an intelligent, optimizing compiler in order to realize anywhere near
|
rlm@421
|
39 their peak performance
|
rlm@421
|
40 \cite{ellis:bulldog,pet87,coutant:precision-compilers}. In these cases, the
|
rlm@421
|
41 compiler not only is responsible for faithfully generating native code to
|
rlm@421
|
42 match the source language, but also must be aware of instruction latencies,
|
rlm@421
|
43 delayed branches, pipeline stages, and a multitude of other factors in order
|
rlm@421
|
44 to generate fast code \cite{gib86}.
|
rlm@421
|
45
|
rlm@421
|
46 Taking these ideas one step further, it seems that the floating point
|
rlm@421
|
47 operations that are normally single, large instructions can be further broken
|
rlm@421
|
48 down into smaller, simpler, faster instructions, with more control in the
|
rlm@421
|
49 compiler and less in the hardware. This is the idea behind a
|
rlm@421
|
50 micro-optimizing FPU; break the floating point instructions down into their
|
rlm@421
|
51 basic components and use a small, fast implementation, with a large part of
|
rlm@421
|
52 the burden of hardware allocation and optimization shifted towards
|
rlm@421
|
53 compile-time.
|
rlm@421
|
54
|
rlm@421
|
55 Along with the hardware speedups possible by using a $\mu$FPU, there are
|
rlm@421
|
56 also optimizations that the compiler can perform on the code that is
|
rlm@421
|
57 generated. In a normal sequence of floating point operations, there are
|
rlm@421
|
58 many hidden redundancies that can be eliminated by allowing the compiler to
|
rlm@421
|
59 control the floating point operations down to their lowest level. These
|
rlm@421
|
60 optimizations are described in detail in section~\ref{ch1:opts}.
|
rlm@421
|
61
|
rlm@421
|
62 \section{Description of micro-optimization}\label{ch1:opts}
|
rlm@421
|
63
|
rlm@421
|
64 In order to perform a sequence of floating point operations, a normal FPU
|
rlm@421
|
65 performs many redundant internal shifts and normalizations in the process of
|
rlm@421
|
66 performing a sequence of operations. However, if a compiler can
|
rlm@421
|
67 decompose the floating point operations it needs down to the lowest level,
|
rlm@421
|
68 it then can optimize away many of these redundant operations.
|
rlm@421
|
69
|
rlm@421
|
70 If there is some additional hardware support specifically for
|
rlm@421
|
71 micro-optimization, there are additional optimizations that can be
|
rlm@421
|
72 performed. This hardware support entails extra ``guard bits'' on the
|
rlm@421
|
73 standard floating point formats, to allow several unnormalized operations to
|
rlm@421
|
74 be performed in a row without the loss information\footnote{A description of
|
rlm@421
|
75 the floating point format used is shown in figures~\ref{exponent-format}
|
rlm@421
|
76 and~\ref{mantissa-format}.}. A discussion of the mathematics behind
|
rlm@421
|
77 unnormalized arithmetic is in appendix~\ref{unnorm-math}.
|
rlm@421
|
78
|
rlm@421
|
79 The optimizations that the compiler can perform fall into several categories:
|
rlm@421
|
80
|
rlm@421
|
81 \subsection{Post Multiply Normalization}
|
rlm@421
|
82
|
rlm@421
|
83 When more than two multiplications are performed in a row, the intermediate
|
rlm@421
|
84 normalization of the results between multiplications can be eliminated.
|
rlm@421
|
85 This is because with each multiplication, the mantissa can become
|
rlm@421
|
86 denormalized by at most one bit. If there are guard bits on the mantissas
|
rlm@421
|
87 to prevent bits from ``falling off'' the end during multiplications, the
|
rlm@421
|
88 normalization can be postponed until after a sequence of several
|
rlm@421
|
89 multiplies\footnote{Using unnormalized numbers for math is not a new idea; a
|
rlm@421
|
90 good example of it is the Control Data CDC 6600, designed by Seymour Cray.
|
rlm@421
|
91 \cite{thornton:cdc6600} The CDC 6600 had all of its instructions performing
|
rlm@421
|
92 unnormalized arithmetic, with a separate {\tt NORMALIZE} instruction.}.
|
rlm@421
|
93
|
rlm@421
|
94 % This is an example of how you would use tgrind to include an example
|
rlm@421
|
95 % of source code; it is commented out in this template since the code
|
rlm@421
|
96 % example file does not exist. To use it, you need to remove the '%' on the
|
rlm@421
|
97 % beginning of the line, and insert your own information in the call.
|
rlm@421
|
98 %
|
rlm@421
|
99 %\tagrind[htbp]{code/pmn.s.tex}{Post Multiply Normalization}{opt:pmn}
|
rlm@421
|
100
|
rlm@421
|
101 As you can see, the intermediate results can be multiplied together, with no
|
rlm@421
|
102 need for intermediate normalizations due to the guard bit. It is only at
|
rlm@421
|
103 the end of the operation that the normalization must be performed, in order
|
rlm@421
|
104 to get it into a format suitable for storing in memory\footnote{Note that
|
rlm@421
|
105 for purposed of clarity, the pipeline delays were considered to be 0, and
|
rlm@421
|
106 the branches were not delayed.}.
|
rlm@421
|
107
|
rlm@421
|
108 \subsection{Block Exponent}
|
rlm@421
|
109
|
rlm@421
|
110 In a unoptimized sequence of additions, the sequence of operations is as
|
rlm@421
|
111 follows for each pair of numbers ($m_1$,$e_1$) and ($m_2$,$e_2$).
|
rlm@421
|
112 \begin{enumerate}
|
rlm@421
|
113 \item Compare $e_1$ and $e_2$.
|
rlm@421
|
114 \item Shift the mantissa associated with the smaller exponent $|e_1-e_2|$
|
rlm@421
|
115 places to the right.
|
rlm@421
|
116 \item Add $m_1$ and $m_2$.
|
rlm@421
|
117 \item Find the first one in the resulting mantissa.
|
rlm@421
|
118 \item Shift the resulting mantissa so that normalized
|
rlm@421
|
119 \item Adjust the exponent accordingly.
|
rlm@421
|
120 \end{enumerate}
|
rlm@421
|
121
|
rlm@421
|
122 Out of 6 steps, only one is the actual addition, and the rest are involved
|
rlm@421
|
123 in aligning the mantissas prior to the add, and then normalizing the result
|
rlm@421
|
124 afterward. In the block exponent optimization, the largest mantissa is
|
rlm@421
|
125 found to start with, and all the mantissa's shifted before any additions
|
rlm@421
|
126 take place. Once the mantissas have been shifted, the additions can take
|
rlm@421
|
127 place one after another\footnote{This requires that for n consecutive
|
rlm@421
|
128 additions, there are $\log_{2}n$ high guard bits to prevent overflow. In
|
rlm@421
|
129 the $\mu$FPU, there are 3 guard bits, making up to 8 consecutive additions
|
rlm@421
|
130 possible.}. An example of the Block Exponent optimization on the expression
|
rlm@421
|
131 X = A + B + C is given in figure~\ref{opt:be}.
|
rlm@421
|
132
|
rlm@421
|
133 % This is an example of how you would use tgrind to include an example
|
rlm@421
|
134 % of source code; it is commented out in this template since the code
|
rlm@421
|
135 % example file does not exist. To use it, you need to remove the '%' on the
|
rlm@421
|
136 % beginning of the line, and insert your own information in the call.
|
rlm@421
|
137 %
|
rlm@421
|
138 %\tgrind[htbp]{code/be.s.tex}{Block Exponent}{opt:be}
|
rlm@421
|
139
|
rlm@421
|
140 \section{Integer optimizations}
|
rlm@421
|
141
|
rlm@421
|
142 As well as the floating point optimizations described above, there are
|
rlm@421
|
143 also integer optimizations that can be used in the $\mu$FPU. In concert
|
rlm@421
|
144 with the floating point optimizations, these can provide a significant
|
rlm@421
|
145 speedup.
|
rlm@421
|
146
|
rlm@421
|
147 \subsection{Conversion to fixed point}
|
rlm@421
|
148
|
rlm@421
|
149 Integer operations are much faster than floating point operations; if it is
|
rlm@421
|
150 possible to replace floating point operations with fixed point operations,
|
rlm@421
|
151 this would provide a significant increase in speed.
|
rlm@421
|
152
|
rlm@421
|
153 This conversion can either take place automatically or or based on a
|
rlm@421
|
154 specific request from the programmer. To do this automatically, the
|
rlm@421
|
155 compiler must either be very smart, or play fast and loose with the accuracy
|
rlm@421
|
156 and precision of the programmer's variables. To be ``smart'', the computer
|
rlm@421
|
157 must track the ranges of all the floating point variables through the
|
rlm@421
|
158 program, and then see if there are any potential candidates for conversion
|
rlm@421
|
159 to floating point. This technique is discussed further in
|
rlm@421
|
160 section~\ref{range-tracking}, where it was implemented.
|
rlm@421
|
161
|
rlm@421
|
162 The other way to do this is to rely on specific hints from the programmer
|
rlm@421
|
163 that a certain value will only assume a specific range, and that only a
|
rlm@421
|
164 specific precision is desired. This is somewhat more taxing on the
|
rlm@421
|
165 programmer, in that he has to know the ranges that his values will take at
|
rlm@421
|
166 declaration time (something normally abstracted away), but it does provide
|
rlm@421
|
167 the opportunity for fine-tuning already working code.
|
rlm@421
|
168
|
rlm@421
|
169 Potential applications of this would be simulation programs, where the
|
rlm@421
|
170 variable represents some physical quantity; the constraints of the physical
|
rlm@421
|
171 system may provide bounds on the range the variable can take.
|
rlm@421
|
172 \subsection{Small Constant Multiplications}
|
rlm@421
|
173
|
rlm@421
|
174 One other class of optimizations that can be done is to replace
|
rlm@421
|
175 multiplications by small integer constants into some combination of
|
rlm@421
|
176 additions and shifts. Addition and shifting can be significantly faster
|
rlm@421
|
177 than multiplication. This is done by using some combination of
|
rlm@421
|
178 \begin{eqnarray*}
|
rlm@421
|
179 a_i & = & a_j + a_k \\
|
rlm@421
|
180 a_i & = & 2a_j + a_k \\
|
rlm@421
|
181 a_i & = & 4a_j + a_k \\
|
rlm@421
|
182 a_i & = & 8a_j + a_k \\
|
rlm@421
|
183 a_i & = & a_j - a_k \\
|
rlm@421
|
184 a_i & = & a_j \ll m \mbox{shift}
|
rlm@421
|
185 \end{eqnarray*}
|
rlm@421
|
186 instead of the multiplication. For example, to multiply $s$ by 10 and store
|
rlm@421
|
187 the result in $r$, you could use:
|
rlm@421
|
188 \begin{eqnarray*}
|
rlm@421
|
189 r & = & 4s + s\\
|
rlm@421
|
190 r & = & r + r
|
rlm@421
|
191 \end{eqnarray*}
|
rlm@421
|
192 Or by 59:
|
rlm@421
|
193 \begin{eqnarray*}
|
rlm@421
|
194 t & = & 2s + s \\
|
rlm@421
|
195 r & = & 2t + s \\
|
rlm@421
|
196 r & = & 8r + t
|
rlm@421
|
197 \end{eqnarray*}
|
rlm@421
|
198 Similar combinations can be found for almost all of the smaller
|
rlm@421
|
199 integers\footnote{This optimization is only an ``optimization'', of course,
|
rlm@421
|
200 when the amount of time spent on the shifts and adds is less than the time
|
rlm@421
|
201 that would be spent doing the multiplication. Since the time costs of these
|
rlm@421
|
202 operations are known to the compiler in order for it to do scheduling, it is
|
rlm@421
|
203 easy for the compiler to determine when this optimization is worth using.}.
|
rlm@421
|
204 \cite{magenheimer:precision}
|
rlm@421
|
205
|
rlm@421
|
206 \section{Other optimizations}
|
rlm@421
|
207
|
rlm@421
|
208 \subsection{Low-level parallelism}
|
rlm@421
|
209
|
rlm@421
|
210 The current trend is towards duplicating hardware at the lowest level to
|
rlm@421
|
211 provide parallelism\footnote{This can been seen in the i860; floating point
|
rlm@421
|
212 additions and multiplications can proceed at the same time, and the RISC
|
rlm@421
|
213 core be moving data in and out of the floating point registers and providing
|
rlm@421
|
214 flow control at the same time the floating point units are active. \cite{byte:i860}}
|
rlm@421
|
215
|
rlm@421
|
216 Conceptually, it is easy to take advantage to low-level parallelism in the
|
rlm@421
|
217 instruction stream by simply adding more functional units to the $\mu$FPU,
|
rlm@421
|
218 widening the instruction word to control them, and then scheduling as many
|
rlm@421
|
219 operations to take place at one time as possible.
|
rlm@421
|
220
|
rlm@421
|
221 However, simply adding more functional units can only be done so many times;
|
rlm@421
|
222 there is only a limited amount of parallelism directly available in the
|
rlm@421
|
223 instruction stream, and without it, much of the extra resources will go to
|
rlm@421
|
224 waste. One process used to make more instructions potentially schedulable
|
rlm@421
|
225 at any given time is ``trace scheduling''. This technique originated in the
|
rlm@421
|
226 Bulldog compiler for the original VLIW machine, the ELI-512.
|
rlm@421
|
227 \cite{ellis:bulldog,colwell:vliw} In trace scheduling, code can be
|
rlm@421
|
228 scheduled through many basic blocks at one time, following a single
|
rlm@421
|
229 potential ``trace'' of program execution. In this way, instructions that
|
rlm@421
|
230 {\em might\/} be executed depending on a conditional branch further down in
|
rlm@421
|
231 the instruction stream are scheduled, allowing an increase in the potential
|
rlm@421
|
232 parallelism. To account for the cases where the expected branch wasn't
|
rlm@421
|
233 taken, correction code is inserted after the branches to undo the effects of
|
rlm@421
|
234 any prematurely executed instructions.
|
rlm@421
|
235
|
rlm@421
|
236 \subsection{Pipeline optimizations}
|
rlm@421
|
237
|
rlm@421
|
238 In addition to having operations going on in parallel across functional
|
rlm@421
|
239 units, it is also typical to have several operations in various stages of
|
rlm@421
|
240 completion in each unit. This pipelining allows the throughput of the
|
rlm@421
|
241 functional units to be increased, with no increase in latency.
|
rlm@421
|
242
|
rlm@421
|
243 There are several ways pipelined operations can be optimized. On the
|
rlm@421
|
244 hardware side, support can be added to allow data to be recirculated back
|
rlm@421
|
245 into the beginning of the pipeline from the end, saving a trip through the
|
rlm@421
|
246 registers. On the software side, the compiler can utilize several tricks to
|
rlm@421
|
247 try to fill up as many of the pipeline delay slots as possible, as
|
rlm@421
|
248 seendescribed by Gibbons. \cite{gib86}
|
rlm@421
|
249
|
rlm@421
|
250
|