| main page (gr) | home | main page |
| novel (gr) | selections | readings | gallery | pre-meaning | advanced computing | str (gr) | internet epitome (gr) | win stuff |
| lecture #1 | lecture #2 | lecture #3 | lecture #4 | lecture #5 |

Advanced Computing Course - Lecture #3
Copyright © Constantin S. Chassapis - amorphous.art
Version 1: 30/11/94. Version 2: May 4, 2000.
This lecture is targeted towards understanding why arithmetic is not quite what we know it to be, (say, from a mathematical analysis or number theory textbook), when working on a computer. We will understand what exactly is the arithmetic precision of a computer, what that really means, and how all that stuff is translated when forming rules for numerical software engineering.
We will start by seeing the basic elements of the internal structure of the central processing unit (cpu) of a computer, and how the hardware imposes limits on arithmetic.
The processor that we shall consider [1], is conceptually similar to the MC6800 8-bit microprocessor, but this does not restricts the generality of the elements that we shall see. The input-output model for the simplified processor is shown in the next figure.

The input signal ADD/SUB is a 1-bit signal that defines the type of operation. If it is active, the processor will add the two input data words (a word here signifies a collection of 8 bits), otherwise, it will subtract the data word at B from the data word at A. The STROBE input is a timing signal indicating when the other inputs are valid. The data inputs A and B are each 8 bits long. The result R also contains 8 bits. The four single-bit output signals CARRY, OVERFLOW, MINUS, and ZERO indicate the status of the result. The READY signal is a timing signal indicating when the data output and the status outputs are valid and when the processor is ready for new input information.
The timing relationships for the i.-o. signals of the simplified processor unit (spu hereon) are shown in the next figure.

The cross hatched areas indicate time intervals where the signal values are undefined. Signals shown as two parallel lines (one high and one low) indicate stable values defined as high or low, the specific value being determined by the data at the time of operation. Any external device providing input signals to the spu must monitor the READY output from the processor and withhold any inputs until this signal becomes active. It must also present stable values on the A lines, B lines and the ADD/SUB line before it activates the STROBE line.
When a STROBE input is detected, the spu stores the input data and inactivates the READY line. This transition indicates that the A, B, ADD/SUB and STROBE lines no longer need to be held stable. When the spu has completed the specified operation, the result R and all status outputs are established before the READY signal is activated. An active READY indicates not only that the outputs are valid but also that another transaction may begin.
This verbal description can be used to derive a block diagram of the spu, as shown in the next figure. The development of such a block diagram is not a systematic procedure but depends upon the experience and point of view of the designer.

The A, B, and status registers are more or less standard registers (i.e. collections of 8 1-bit flip-flops with the necessary control logic embodied). The A register may be loaded from one of two sources by way of a multiplexer (MUX) (A multiplexer or, data selector, is a device with multiple data input lines which effectively can be connected to a single data output line. The selection is controlled by an additional set of input lines, called data-select or control lines. The behaviour of a two 1-bit to one 1-bit multiplexer can be described by the equation (positive logic assumed): Z = ( NOT(S) AND D0 ) OR ( S AND D1 ), where the S line is the select line and the D lines are the two 1-bit input lines. Z is of course the data output line.) If the control input to the multiplexer, SELECT INPUT is high, the external input at A is selected, otherwise the output of the adder is selected. The A register is loaded when the control signal LOAD DATA is active. The B register may be loaded with external data from the B input lines, or the contents of the register may be replaced by the corresponding twos-complement (In general, codes to represent both positive and negative integer values use the most significant bit (msb) as a sign bit. With the most direct approach, this bit is 0 for a positive value and a 1 for negative values, the magnitude of the number is represented by the remaining bits. This approach is called the "binary-value sign-magnitude code". Although this approach is conceptually the simplest, it requires complicated electornics to be implemented. Another code for representing both positive and negative integer values is the "binary-value ones-complement code". Positive numbers are represented using the binary-value code witth the restriction that the msb must be a 0. The code for a negative value is obtained by complementing each bit in the code word for the corresponding positive number. Each bit is complemented by changing 0 to 1 and vice versa. For example, with a 4-bit word, +4 is 0100 and -4 is 1011. Since the msd is always 0 for positive numbers, it will always be 1 for negative numbers, thus the msb can still be considered as a sign bit. Although the electronic implementation for arithmetic operations with the binary-value ones-complemented code is simpler than the direct approach we saw before, other inconveniences inhibit its use. The most common code for representing both positive and negative integer values is the "binary-value twos-complement" code. Its is similar to the ones-complement code except that the code word for a negative number is incremented by 1. For example, the 4-bit code word for -4 is 1100, which is obtained by first complementing the code word for +4, 0100, to obtain the 1011 and then adding 1. This operation is called negation. As in the other two codes, the msb distinguishes positive and negative values. With this code, arithmetic operations are relative easy to implement in hardware) value for use in subtraction. The two control inputs LOAD DATA and NEGATE define when these operations are performed. The status register is loaded with the output of the status decoder when the control signal LOAD STATUS becomes active. This register is used to store the status of the last operation, otherwise the correct status would be available only for a very short time.
The design of the control unit is interesting but we will not present it here. For completeness of the discussion we will briefly see its structural model. (Note that the Q’s decode the “present status” of the controller, and “combinational network” is a network whose outputs depend only on the current input).

The reason we made the previous discussion is the following. We needed to discuss the structure of a cpu so to understand what the registers do, and so to understand in what way the finite aspect of the registers limits our number manipulation operations we do with a computer and how to overcome these limits.
The cpu uses registers to store numeric or non-numeric information. Then it uses its arithmetic-logic unit (alu) to operate on the contents of the registers. These operations are very fast. And we use almost all the time the cpu directly to perform computations. Due to the finite aspect of the registers (8-bit, 16-bit, 32-bit, or 64-bit) the numbers that can be accurately represented are bounded. For example, the biggest in magnitude signed integer that can be represented by a 32-bit machine is ±231 or, ±2 147 483 648. These are the numbers that fortran sees directly. If we want to do higher precision arithmetic, say 200-bit arithmetic, we are obliged to change our way of looking on numbers and find efficient ways to split our numbers to 32-bit pieces, so to use the fast operations that the cpu is able to perform. If we are not so interested in speed, we may even represent our high precision numbers as fortran arrays and then operate on them on a element by element basis (i.e. digit by digit basis), not using the alu directly but (very) indirectly. In this last case we do not care to split our numbers and we do not care if the cpu is 8-bit or 64-bit.

There may be other number-theoretic ways to do high precision arithmetic, but the essential to be understood here is that we need to rethink our numbers if high-precision is needed, and we need to double-rethink our numbers if speed and high-precision are needed together. The limits we mentioned in the beginning of thi section, are only limits connected with the speed of operations, and therefore, not real limits.
In what follows we will concentrate to understand how to use “on the maximum” our restricted-precision arithmetic, leaving the “infinite”-precision arithmetic (via the use of higher-level constructs or languages or mathematics) for a future lecture. This understanding requires first that we understand what type of errors are introduced due to the finite aspect of the machines.
The representation of integers
As we already saw on the first lecture, an unsigned integer number d is represented by a relation of the form
(1) (d)b = (dn ... d1d0)b = dnbn + ... + d1b1 + d0b0
where b is the base and di may be any digit from {0, 1, 2, ..., b-1}. As an example, let us see what happens if one needs to find the representation of (d)b in a new base ß. One just re-uses (1) using base ß inside (1). More precisely,
(2) (d)b = (dn)ß(b)ßn + ... + (d1)ß(b)ß1 + (d0)ß(b)ß0 = ... = Dmßm + ... + D1ß1 + D0ß0
where now the m digits Di belong to the set {0, 1, 2, ..., ß-1}.
In what follows, when we will write a number without explicitly stating the base, we will imply a base ten number.
The representation of fractions
If now x is a positive real number, then its integral part xI is the largest integer less than or equal to x, while
(3) xF = x - xI
is its fractional part. The fractional part can always be written as a decimal fraction:
(4) xF = d110-1 + d210-2 + d310-3 + ...
where each di is a non-negative integer less than 10. If di= 0 for all i greater than a certain integer, then the fraction is said to terminate. Thus 1/4 is a terminating decimal fraction (=0.25), while 1/3 (=0.333...) is not.
If the integral part of x is given as a decimal integer by
(5) xI = (an ... a1a0)10
while the fractional part is given by equation (4), we usually write them together, separating them by a point, the famous "decimal point":
(6) x = (an an-1 ... a0 . d1d2d3 ...)10
Completely analogously one may write [2]:
(7) x = (an an-1 ... a0 . d1d2d3 ...)2
where equation (4) is assumed now written on base 2 and each di or ai in (7) is either 0, or 1.
The binary fraction (.d1d2d3 ...)2 for a given number xF between zero and 1 can be calculated as follows: if
(8) xF = d12-1 + d22-2 + d32-3 + ...
then
(9) 2xF = d121-1 + d221-2 + d321-3 + ... = d1 + d22-1 + d32-2 + ...
Hence d1 is the integral part of 2xF, while
(10) (2xF)F = 2xF - d1 = d22-1 + d32-2 + d42-3 + ...
So, repeating this procedure, we find that d2 is the integral part of 2(2xF)F, d3 is the integral part of 2(2(2xF)F)F, etc.
If, for example, x = 0.625 = xF, then
2(0.625) = 1.25, so d1 = 1
2(0.25) = 0.5, so d2 = 0
2(0.5) = 1.0, so d3 = 1
and all further di’s are zero. Hence
(0.625)10 = (.101)2
This example was formed to give a terminating binary fraction. Unhappily, not every terminating decimal fraction gives rise to a terminating binary fraction. This is due to the fact that the binary fraction for xF = 10-1 = 0.1, is not terminating! We have
2(0.1) = 0.2, so d1 = 0,
2(0.2) = 0.4, so d2 = 0,
2(0.4) = 0.8, so d3 = 0,
2(0.8) = 1.6, so d4 = 1,
2(0.6) = 1.2, so d5 = 1,
and now we are back to a fractional part of 0.2, so, the digits cycle. It follows that
(0.1)10= (.0 0011 0011 0011 ...)2
The procedure just outlined is formalised in the following algorithm:
Algorithm 1: Given x between 0 and 1 and an integer ß greater than 1, Generate recursively the digits d1, d2, ..., by: c0 <- x d1 <- (ßc0)I, c1 <- (ßc0)F d2 <- (ßc1)I, c2 <- (ßc1)F ... Then x = (.d1d2d3...)ß
We have expressed this algorithm using a general base ß for two reasons. If this conversion to the binary system is carried out with pencil and paper, it is usually faster to convert first to octal, i.e. use ß=8, and then to convert from octal to binary (The octal number system presents a kind of compromise between the computer-preffered binary and the people-preffered decimal system. It is easy to convert from octal to binary and back since three binary digits make an octal digit. To convert from octal to binary, one merely replaces all octal digits by their binary equivalent, thus, (347)8= (011 100 111)2= (011100111)2. Conversely, to convert from binary to octal, one partitions the binary digits in groups of three (starting from the right) and then replaces each three-group by its octal digit, thus, (10111011)2 = (010 111 011)2 = (273)8). Also, the algorithm can be used to convert a binary (or octal) fraction to decimal, by choosing ß=10 and using binary (or octal arithmetic) in the spirit of (2).
Note that if xF is a terminating binary fraction with n digits, then it is also a terminating decimal fraction with n digits, since (.1)2= 0.5.
Floating-point representation
As we have discussed already, digital computers must make do with a fixed finite number of places, the word length, when internally representing a number. This number n is determined by the make of the machine, although some machines have build-in extensions to integer multiples 2n, 3n, .. (double word length, triple word length, ...) of n to offer greater precision if needed (At the cost of reducing the overall speed of the program ofcourse, since now the low-level operations will have to be made on the double or on the triple. This is the compromise being done when for example one uses the (standard) double precision type in fortran. The least, you will double execution time. Typically you will multiply execution time by more than two, because not all machines have build-in the double word length option and therefore, some really complicated machine code will have to be produced by the compiler!). A word length of n places can be used in several different fashions to represent a number.
The so-called fixed-point representation [3] specifies a fixed number r of places before and a fixed number q after the decimal (binary) point, so that r + q = n. In this representation, the position of the decimal (binary) point is fixed. A few simple digital devices, mainly for accounting purposes, are still restricted to fixed-point representation. Much more important, in particular for scientific calculations, are digital computers featuring floating-point representation of numbers. Here, the decimal (binary) point is not fixed at the outset, rather its position with respect to the first digit is indicated for each number separately. This is done by specifying a so-called exponent. In other words, each real number can be represented in the form
(11) x = a x10b, |a| < 1, b integer
(say, 30.421 is 0.3041 x10-2), where b indicates the position of the decimal point with respect to the mantissa a. On any digital computer there are, of course, only fixed finite numbers t+1 and p, (with t + 1 + p = n) of places available for the representation of the sign and of the mantissa and exponent respectively. The number t is usually called the precision of the floating-point number system.
A floating-point representation is called normalised if the first digit (bit) of mantissa is different from zero. Then |a| ≥ 10-1 (or, if base = 2, then |a| ≥ 2-1) holds in eq. (11). The significant digits (bits) of a number are the digits of the mantissa not counting leading zeros.
In what follows, we will only consider normalised floating-point representations and the corresponding arithmetic. The numbers t and p determine, together with the basis ß of the representation, the subset A of real numbers which can be represented exactly by a given machine. The elements of A are called the machine numbers.
We may write a t-digit floating point number x in base ß with more detail in the form
(12) x = ± (.d1d2...dt)ß ße
where (.d1d2...dt)ß is a ß-fraction (the mantissa) satisfying (normalisation)
(13) ß-1≤ dtßt + ... + d2ß2 + d1ß1 < 1
and e is an integer (the exponent). The exponent e is limited (due to finite p) to a range
(14) m ≤ e ≤ M
for certain integers m and M. Usually m = -M and m = 2p-1.
If ß = 2, the normalised representation may be in a given computer something like:

For example [4], consider the floating-point number system determined by ß = 2, t = 3 and M = -m = 1. This system contains zero (a not-normalisable number! remember the ZERO status bit in the spu) and all numbers with representation
x = ± (.1d2d3)2 2e
where -1 ≤ e ≤ 1 and d2 and d2 are each 0 or 1. Thus for nonzero x there are two choices for the sign, three choices for the exponent e, and four choices for the floating point 2-fraction, giving 1 + 2 x 3 x 4 = 25 floating-point numbers. In other words, A, in this case, has only 25 members! Converting these to their decimal representation, the four permissible floating-point fractions (.1d2d3)2 are 1/2, 5/8, 3/4, and 7/8. For example, 0.101 = 5/8. Thus, the smallest positive floating-point number is here ë = 2-1 x 1/2 = 0.25 while the largest is Ë = 2+1 x 7/8 = 1.75. The nonnegative floating-point numbers can be depicted as shown in the next figure

This picture is misleading in that the distance å from 1 to the next larger floating point number turns out to be equal to ë in this example. Moreover, the picture does not highlight the unique character of the spacing between zero and its neighbours. In a floating-point number system with ß=10, t=6 and M=-m=100, which gives a much better feel for realistic floating-point systems, we would have, å = 10-5 and ë = 10-101. Moreover, there would be no floating-point numbers between 0 and ë, but 899 999 of them between ë and 10ë.
The distance å from 1 to the next larger floating point number is called machine epsilon. It is probably the most useful quantity associated with a floating-point number system because it gives a measurement of the "granularity" of the system that is valid over the entire range of nonzero machine numbers.
The most important fact about floating-point number systems [4]: The spacing between a floating-point number x and an adjacent floating-point number is at least å|x|/ß and at most å|x| (unless x or the neighbour is 0).
Thus the spacing å/ß to the left of 1.0 and the spacing å to the right of 1.0 are the extreme cases of relative spacing between consecutive nonzero floating point numbers.
One also finds the following definition for å [3]:
(15) å = min { g in A with 1 + g > 1 and g > 0 }
where the symbol + here denotes the "floating-point addition".
The following relations hold [4]:
(16) ë = ßm-1
(17) Ë = ßM(1-ß)-t
(18) If m ≤ e < M, then there are exactly (ß-1)ßt-1+1 floating-point numbers x satisfying ße-1 ≤ x ≤ ße and these numbers have equal spacing ße-t.
(19) å = ß1-t.
Computing arithmetic parameters
Programs can compute the values (for the machine on which the program is run) of certain environmental parameters. The program segment given below [4], uses the fact that the integers that can be represented as floating-point numbers are
1, 2, ..., ßt-1, ßt, ßt+ß, ßt+2ß, ..., ßt+1, ßt+1+ß, ßt+1+ß2, ... .
so to determine the base ß and precision t. For example, on a three-digit decimal system (t=3, ß=10) the representable integers are 1, 2, 999, 1000, 1010, 1020, ...,10000, 10100, ... .
The following module keeps doubling A until it reaches a point where the spacing between consecutive floating-point numbers exceeds 1. It then looks for the next larger floating-point number, from which it subtracts A to get ß. At that point t can be determined as the smallest power of ß for which the distance to the next floating-point number exceeds 1.
subroutine baspre (base, prec)
implicit none
integer base, prec
real A, B
A = 1.0
10 A = 2.0*A
if ( (A+1.0)-A .eq. 1.0 ) goto 10
B = 1.0
20 B = 2.0*B
if ( A+B .eq. A ) goto 20
base = (A+B) - A
prec = 0
A = 1.0
30 prec = prec + 1
A = A*base
if ( (A+1.0)-A .eq. 1.0) goto 30
end
If ß and t are known, then å can be determined from the formula å = ß1-t. Alternatively å can be computed directly by the following program:
subroutine epsil ( eps )
implicit none
real try, try1, eps, eps1
try = 1.0
10 eps = try
try = 0.5*eps
try1 = try + 1.0
if ( try1 .gt. 1.0 ) goto 10
eps1 = eps + 1.0
eps = eps1 - 1.0
end
Floating-point arithmetic
Having said all that about machine numbers, one poses the question "how to approximate a number x that does not belongs to A by a number g that belongs to A" [3]. This problem is encountered not only when reading data into a computer, but also when representing intermediate results within the computer during the course of a calculation. Indeed, straightforward examples show that the results of elementary arithmetic operations like x+y, or x/y, need not belong to A, even if both operands are machine numbers.
It is natural to postulate that the approximation of any number x that does not belongs to A by a machine number fl(x) in A should satisfy
(20) | x - fl(x) | ≤ | x - g | for all g in A
Such a machine number approximation can be obtained in most cases by rounding, where fl(x) is chosen as the normalised floating-point number nearest to x.
The difference between x and fl(x) is called round-off error. The round-off error depends on the magnitude of x and is therefore best measured relative to x. For if we write
(21) fl(x) = x (1 + ä)
where ä = ä(x), then it is possible to bound ä independently of x. Usually fl(x) works in such a way so to ensure that |ä| ≤ å.
Similar relations to (21) hold for the elementary arithmetic operations (+, -, *, /) on a computer. If by +, -, *, / we denote the machine operations corresponding to +, -, *, /, then, for example,
(22) x*y = fl(x*+y)
with similar relations being true for the other three operations. Eq. (22) implies that
(23) x*y = (x*y)(1 + ä), for some ä with |ä| ≤ å
This equation can also be interpreted in the sense that the floating-point operation * (and likewise -, +, /) is exact, only that it applies to slightly perturbed operands. In other words, x * y = x’ * y’ = (x+äx) * (y+äy).
If the number x* is an approximation to the exact answer x, then we call the difference x-x* the error in x*. The relative error in x* as an approximation to x, is defined to be the number (x-x*)/x. This number is close to the number (x-x*)/x* if it is at all small.
Every elementary floating-point operation in a computational process may give rise to an error ä which, once generated, may then be amplified or reduced in subsequent operations.
One of the most common and often avoidable way of increasing the importance of an error is commonly called "loss of significant digits". If x* is an approximation to x, then we say that x* approximates x to r significant ß-digits provided that
(24) | x - x* | ≤ (1/2) ßs-r+1
with s the largest integer such that ßs≤ |x|. For instance, x*=3 agrees with x=3,14... to one significant decimal digit, while, x*=22/7 = 3.1428..., is correct to three significant digits.
Suppose now that we are to calculate the number z = x - y and that we have approximations x* and y* for x and y respectively, available, each of which is good to r digits. Then, z* = x* - y* is an approximation for z, which is also good to r digits unless x* and y* agree to one or more digits. In this latter case, there will be cancellation of digits during the subtraction, and consequently z* will be accurate to fewer than r digits.
Consider for example, x* = 0.76545421 x101 and y* = 0.76544200 x101 and assume each to be an approximation to x and y, respectively, correct to seven significant digits. Then, in eight-digit floating-point arithmetic (i.e. t=8, ß=10), z* = x* - y* = 0.12210000 x10-3 is the exact difference between x* and y*. But as an approximation to z=x-y, z* is good only to three digits, since the fourth significant digit of z* is derived from the eighth digits of x* and y*, both possibly in error. Hence, while the error in z* (as an approximation to z=x-y) is at most the sum of errors in x* and y*, the relative error in z* is possibly 10 000 times the relative error in x* or y* !!
Such loss can be avoided by anticipating its occurrence. Consider for example, the evaluation of the function f(x)=1-cos(x) in six decimal digits arithmetic. Since cos(x) ~ 1 for x ~ 0, there will be loss of significant digits for x near zero. We should use for example the equivalent formula f(x) = sin/(1+cos(x)) which can be evaluated quite accurately for small x. Alternatively one could use the Taylor expansion of cos(x) to write f(x)=x2/2 which shows, for example, that for |x|≤ 10-3, x2/2 agrees with f(x) to at least six significant digits.
Another good example is provided by the problem of finding the roots of the quadratic equation a x2 + b x + c = 0. The solution, is well known from algebra to be x±= (-b±sqrt(b2-4ac))/2a. This solution involves subtraction, and therefore we expect loss of significant digits when calculating x+ if 4ac is much smaller than b. Here too, the loss of significant digits can be avoided if we use the equivalent formula, x+ = (-2c)/(b+sqrt(b2-4ac)).
Once an error is committed it contaminates subsequent results. This error propagation through subsequent calculations is conveniently studied in terms of the two related concepts of condition and instability.
The word "condition" is used here to describe the sensitivity of the function value f(x) to changes in the argument x. The condition is usually measured by the maximum relative change in the function value f(x) caused by a unit relative change in the argument. In a somewhat informal formula, the condition of f at x is
(25) cond[f(x)] = max{|(f(x)-f(x*))/f(x)|/|(x-x*)/x| for |x-x*| "small" } ~ |xf'(x)/f(x)|
The larger the condition, the more ill-conditioned the function is said to be.
If for example, f(x)=sqrt(x) then f'(x)=1/(2sqrt(x)) , hence the condition is, approximately, 1/2. This says that taking square roots is a well-conditioned process since it actually reduces the relative error !!
By contrast, if f(x) = 10/(1-x2), then f'(x) = 20x/(1-x2)2, so that |xf'(x)/f(x)| = 2 x2/|1-x2| and this number can be quite large for |x| near 1. Thus, for x near 1 or -1, this function is ill-conditioned: it strongly amplifies relative errors in the argument there.
All these can be generalised to multi-argument functions. One may consult [3] section 1.3.
The notion of instability describes the sensitivity of a numerical process for the calculation of function f(x) from x to the inevitable rounding errors committed during its execution in finite precision arithmetic. The precise effect of these errors on the accuracy of the computed f(x) is hard to determine in general. It is possible though to estimate it by considering the rounding errors step-by-step. Then, the total process is unstable to the extent that one or more of these steps is ill-conditioned.
We should always keep in mind Murphy’s law of floating-point arithmetic: Anything that can go wrong, does, on some computer.
There are three approaches to estimating errors. Forward error analysis, backward error analysis and experimentation. The next figure [6] presents them graphically.

The forward error is the actual error in the computed solution y. The backward error is how close y comes to exactly solving the original problem. The four pluses depicted are result from other methods and they, along with y, give an idea of how big the forward error is (this is "experimentation", when, for example, we execute the same program in double precision mode).
1. "Introduction to microcomputer-based digital systems", J. W. Gault & R. L. Pimmel, McGraw-Hill, 1982.
2. "Elementary numerical analysis - An algorithmic approach", S. D. Conte & C. de Boor, McGraw-Hill, 1981.
3. "Introduction to numerical analysis", J.Stoer & R. Bulirsch, Springer, 1980.
4. "The engineering of numerical software", W. Miller, Prentice-Hall, 1984.
5. "A first course in numerical analysis", A. Ralston, McGraw-Hill, 1965.
6. "Numerical methods, software and analysis", J. R. Rice, McGraw-Hill, 1983.
If you have something to say about anything in this site, please, communicate! Contact me at cschassapis@acm.org. But, if you download and save this page to your machine, or print this page, then, please send me an e-mail saying that you did that. Thanks.
Last updated: June 7, 2008