Rather than accessing individual bits in memory, most computers use blocks of 8 bits, or bytes, as the smallest addressable unit of memory.
A machine-level program views memory as a very large array of bytes, referred to as virtual memory. Every byte of memory is identified by a unique number, known as its address, and the set of all possible addresses is known as the virtual address space. This virtual address space is just a conceptual image presented to the machine-level program.
In subsequent chapters, we will cover how the compiler and run-time system partitions this memory space into more manageable units to store the different program objects, that is, program data, instructions, and control information. Various mechanisms are used to allocate and manage the storage for different parts of the program This management is all performed within the virtual address space.
Every computer has a word size(字长), indicating the nominal(标称) size of pointer data. Since a virtual address is encoded by such a word, the most important system parameter determined by the word size is the maximum size of the virtual address space.
To avoid the vagaries(奇怪行为) of relying on “typical” sizes and different compiler settings, ISO C99 introduced a class of data types where the data sizes are fixed regardless of compiler and machine settings.
Among these are data types int32_t and int64_t, having exactly 4 and 8 bytes, respectively. Using fixed-size integer types is the best way for programmers to have close control over data representations.
For program objects that span multiple bytes, we must establish two conventions: what the address of the object will be, and how wewill order the bytes in memory. In virtually all machines, a multi-byte object is stored as a contiguous sequence of bytes, with the address of the object given by the smallest address of the bytes
used.
For example, suppose a variable x of type int has address 0x100; that is, the value of the address expression &x is 0x100. Then (assuming data type int has a 32-bit representation) the 4 bytes of x would be stored in memory locations 0x100, 0x101, 0x102, and 0x103.
voidshow_bytes(byte_pointer start, size_t len) { size_t i; // size_t expresses the sizes of data structures(单位:byte) for(i = 0; i < len; ++i) { printf(" %.2x", start[i]); // %.2x indicates that an integer should be printed in hexadecimal with at least 2 digits } printf("\n"); }
A string in C is encoded by an array of characters terminated by the null(having value 0) character.Each character is represented by some standard encoding, with the most common being the ASCII character code.
When compiled on different machines, the generated machine codes are different.
Different machine types use different and incompatible(不兼容的) instructions and encodings. Even identical processors running different operating systems have differences in their coding conventions and hence are not binary compatible.
Binary code is seldom portable across different combinations of machine and operating system. A fundamental concept of computer systems is that a program, from the perspective of the machine, is simply a sequence of bytes. The machine has no information about the original source program, except perhaps some auxiliary(辅助的) tables maintained to aid in debugging.
1.6 Introduction to Boolean Algebra
Since binary values are at the core of how computers encode, store, and manipu
late information, a rich body of mathematical knowledge has evolved around the study of the values 0 and 1.
The simplest Boolean algebra is defined over the two-element set {0, 1}.
与、或、非、异或
We can extend the four Boolean operations to also operate on bit vectors, strings of zeros and ones of some fixed length w.
The Boolean operations |, &, and ~ operating on bit vectors of length w(w>0) form a Boolean algebra.
布尔代数也有很多和对整数运算相同的性质,比如分配律
When we consider operations ^, &, and ~ operating on bit vectors of length w, we get a different mathematical form, known as a Boolean ring(布尔环). Boolean rings have many properties in common with integer arithmetic. For example, one property of integer arithmetic is that every value x has an additive inverse −x, such that x+−x=0. A similar property holds for Boolean rings, where ^ is the “addition” operation, but in this case each element is its own additive inverse. That is, a ^ a = 0 for any value a, where we use 0 here to represent a bit vector of all zeros. We can see this holds for single bits, since 0^0 = 1^1 = 0, and it extends to bit vectors as well. This property holds even when we rearrange terms and combine them in a different order, and so (a ^ b)^ a = b.
1.7 Bit-Level Operations in C
One useful feature of C is that it supports bitwise(按位的) Boolean operations.
voidreverse_array(int a[], int cnt) { int first. last; for (first=0, last=cnt-1; first <= last; first++, last--) { inplace_swap(&a[first], &a[last]); } }
如果数组的元素个数是偶数个,最中间的元素由于自己和自已异或,会变成0,所以应该修改为first < last
One common use of bit-level operations is to implement masking(掩码) operations, where a mask is a bit pattern that indicates a selected set of bits within a word. As an example,the mask 0xFF (having ones for the least significant 8 bits) indicates the low-order byte of a word. The bit-level operation x & 0xFF yields a value consisting of the least significant byte of x, but with all other bytes set to 0. For example, with x = 0x89ABCDEF, the expression would yield 0x000000EF.
The expression ~0 will yield a mask of all ones, regardless of the size of the data representation(即不管字的大小是多少). The same mask can be written 0xFFFFFFFF when data type int is 32 bits, but it would not be as portable.
Problem 1
写出关于x的C语言表达式,x = 0x87654321
你的代码对于字长大于等于8 bit的机器都要成立,方括号以字长为32 bit为例
A. All but the least significant byte of x complemented, with the least significant byte left unchanged. [0x789ABC21]
B. The least significant byte set to all ones, and all other bytes of x left un
changed. [0x876543FF]
A. x ^ ~0xFF
B. x | 0xFF
1.8 Logical Operations in C
C also provides a set of logical operators ||, &&, and !. These can easily be confused with the bit-level operations, but their behavior is quite different. The logical operations treat
any nonzero argument as representing TRUE and argument 0 as representing FALSE. They return either 1 or 0, indicating a result of either TRUE or FALSE, respectively.
The logical operators do not
evaluate their second argument if the result of the expression can be determined
by evaluating the first argument. Thus, for example, the expression a && 5/a will never cause a division by zero, and the expression p && *p++ will never cause the dereferencing(解引用) of a null pointer.
Problem 2
Using only bit-level and logical operations, write a C expression that is equivalent to x == y.In other words, it will return 1 when x and y are equal and 0 otherwise.
! (x ^ y)
因为如果x和y相同,那么x^y就会得到全0,也就是0,!0得到1
1.9 Shift Operations in C
C also provides a set of shift(移位) operations for shifting bit patterns to the left and to the right.
For an operand x having bit representation [xw−1,xw−2,...,x0], the C expression x << k yields a value with bit representation [xw−k−1,xw−k−2,...,x0,0,...,0]. That is, x is shifted k bits to the left, dropping off the k most significant bits and filling the right end with k zeros. The shift amount should be a value between 0 and w−1. Shift operations associate from left to right, so x << j << k is equivalent to (x << j) << k.
Generally, machines support two forms of right shift.
Logical. A logical right shift fills the left end with k zeros.
Arithmetic. An arithmetic right shift fills the left end with k repetitions of the most significant bit.
In this section, we describe two different ways bits can be used to encode integers — one that can only represent nonnegative numbers, and one that can represent negative, zero, and positive numbers.
2.1 Integral Data Types
C supports a variety of integral data types — ones that represent finite ranges of integers.
Each type can specify a size with keyword char, short, long, as well as an indication of whether the represented numbers are all nonnegative (declared as unsigned), or possibly negative (the default.)
2.2 Unsigned Encodings
Let us consider an integer data type of w bits. We write a bit vector as either x to denote the entire vector, or as [xw−1,xw−2,...,x0] to denote the individual bits within the vector. Treating x as a number written in binary notation, we obtain the unsigned interpretation of x.
等号上加一个点意思是左边被定义成等于右边,函数B2Uw是Binary to Unsigned的缩写,长度为w,它将一个长度为w的0、1串映射到非负整数,计算方法就是把0、1串看作二进制表示,例如B2U4([0101])=22+20=5
Function B2Uw is a bijection.
双射的意思是一一对应,不了解的读者可以自行搜索了解,即在0到2w−1之间的整数的w位无符号编码是独一的
2.3 Two’s-Complement Encodings
For many applications, we wish to represent negative values as well. The most common computer representation of signed numbers is known as two’s-complement(补码) form. This is defined by interpreting the most significant bit of the word to have negative weight. We express this interpretation as a function B2Tw (for “binary to two’s complement” length w).
For x=[xw−1,xw−2,...,x0] :
B2Tw(x)≐−xw−12w−1+i=0∑w−2xi2i
The most significant bit xw−1 is also called the sign bit(符号位)
C allows casting between different numeric data types. For example, suppose variable x is declared as int and u as unsigned. The expression (unsigned)x converts the value of x to an unsigned value, and (int)u converts the value of u to a signed integer.
For most implementations of C, the effect of casting is to keep the bit values identical but change how these bits are interpreted.
Although the C standard does not specify a particular representation of signed numbers, almost all machines use two’s
complement. Generally, most numbers are signed by default. For example, when declaring a constant such as 12345 or 0x1A2B, the value is considered signed. Adding character ‘U’ or ‘u’ as a suffix creates an unsigned constant; for example, 12345U or 0x1A2Bu.
When printing numeric values with printf, the directives %d, %u, and %x are used to print a number as a signed decimal, an unsigned decimal, and in hexadecimal format, respectively. Note that printf does not make use of any type information, and so it is possible to print a value of type int with directive %u and a value of type unsigned with directive %d.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
int tx, ty; unsigned ux, uy; ty = -1; ux = pow(2, 31);
printf("ux = %u = %d\n", ux, ux);
tx = ux; /*Cast to signed*/ uy = ty; /*Cast to unsigned*/
In printf("ux = %u = %d\n", ux, ux);, printf prints the word first as if it represented an unsigned number and second as if it represented a signed number.
When an operation is performed where one operand is signed and the other is unsigned, C implicitly casts the signed argument to unsigned and performs the operations assuming the numbers are nonnegative.
It leads to nonintuitive results for relational operators such as < and >.
Looking at the C header file limits.h, we see that they use a similar method as we have to write TMin32 and TMax32 :
1 2 3
/* Minimum and maximum values a 'signed int' can hold. */ #define INT_MAX 2147483647 #define INT_MIN (-INT_MAX-1)
2.6 Expanding the Bit Representation of a Number
One common operation is to convert between integers having different word sizes while retaining the same numeric value. Of course, this may not be possible when the destination data type is too small to represent the desired value. Converting from a smaller to a larger data type, however, should always be possible.
To convert an unsigned number to a larger data type, we can simply add leading zeros to the representation; this operation is known as zero extension(零扩展).
For converting a two’s-complement number to a larger data type, the rule is to perform a sign extension(符号扩展), adding copies of the most significant bit to the representation.
// Output: /* When run as a 64-bit program on a little-endian machine that uses a two's complement representation. */ // sx = -12345: c7 cf // usx = 53191: c7 cf // x = -12345: c7 cf ff ff // ux = 53191: c7 cf 00 00
int x = 53191; /* c7 cf 00 00 */ short sx = (short) x; /* -12345 */ int y = sx; /* -12345 */
Casting x to be short will truncate(截断) a 32-bit int to a 16-bit short. As we saw before, this 16-bit pattern is the two’s-complement representation of −12,345. When casting this back to int, sign extension will set the high-order 16 bits to ones, yielding the 32-bit two’s-complement representation of −12,345.
/* This is buggy code */ floatsum_elements(float a[], unsigned length) { int i; float result = 0; for (i = 0; i <= length - 1; i++) { result += a[i]; } return result; }
An arithmetic operation is said to overflow when the full integer result cannot fit within the word size limits of the data type.
Detecting overflow of unsigned addition
对于0≤x,y≤UMaxw,设s=x+wuy,那么计算s时发生溢出当且仅当s<x(或者s<y,这两者等价)
这是因为如果溢出,则s=x+y−2w,而y−2w<0
比如我们可以写一个判断加法计算是否发生溢出的函数
1 2 3 4 5
/* Determine whether arguments can be added without overflow */ intuadd_ok(unsigned x, unsigned y) { unsigned sum = x + y; return sum >= x; }
Modular addition forms a mathematical structure known as an abelian group(阿贝尔群)
That is, it is commutative(可交换的) and associative(可结合的); it has an identity element 0, and every element has an additive inverse(加法逆元). Let us consider the set of w-bit unsigned numbers with addition operation +wu. For every value x, there must be some value −wux such that −wux+wux=0
Unsigned Negation
For any number x such that 0≤x<2w, its w-bit unsigned negation −wux is:
Given integer values x and y in the range −2w−1≤x,y≤2w−1−1, their sum is in the range −2w≤x+y≤2w−2, potentially requiring w+1 bits to represent exactly.
Let us define x+wty to be the result of truncating the integer sum x+y to be w bits long and then viewing the result as a two’s-complement number.
Two’s-complement addition
For integer values x,y in the range −2w−1≤x,y≤2w−1−1, x+wty=
Detecting overflow of two’s-complement addition
For x and y in the range TMinw≤x,y≤TMaxw,s=x+wty
Then the computation of s has had positive overflow if and only if x>0 and y>0 but s≤0. The computation has had negative overflow if and only if x<0 and y<0 but s≥0
3.3 Two’s-Complement Negation
Every number x in the range TMinw≤x≤TMaxw has an additive inverse under +wt, which we denote −wtx
/* Determine whether arguments can be multiplied without overflow. */ inttmul_ok(int x, int y) { int p = x * y; // Either x is 0, or dividing p by x gives y. return !x || p/x == y; }
Problem 6
假设int是32位的,利用int64_t设计一个检验乘法是否发生溢出的函数,不要用除法
1 2 3 4 5 6 7
inttmul_ok(int x, int y) { // Compute product without overflow. int64_t p = (int64_t) x*y;
// See if casting to int preserves value. return p == (int) p; }
注意第3行的如果写成int64_t p = x*y是不行的,这样的话x*y会先算成一个32位的值(可能是溢出后截断的),然后再位扩展成64位
Problem 7
3.6 Multiplying by Constants
Historically, the integer multiply instruction on many machines was fairly slow, requiring 10 or more clock cycles, whereas other integer operations—such as addition, subtraction, bit-level operations, and shifting—required only 1 clock cycle.
As a consequence, one important optimization used by compilers is to attempt to replace multiplications by constant factors with combinations of shift and addition operations.
Unsigned multiplication by a power of 2
For C variables x and k with unsigned values x and k, such that 0≤k<w, the C expression x << k yields the value x∗wu2k.
使用一定的数学知识即可证明无符号数的情况
Two’s-complement multiplication by a power of 2
For C variables x and k with two’s-complement values x and unsigned value k, such that 0≤k<w, the C expression x << k yields the value x∗wt2k.
其实这个证明也可以不用补码和无符号数乘法结果的bit串一致的性质,直接展开用数学证明也行
Integer division on most machines is even slower than integer multiplication — requiring 30 or more clock cycles.
整数除法总是舍入到0,即如果x/y结果是正数,向下取整,如果x/y结果是负数,向上取整
Unsigned division by a power of 2
For C variables x and k with unsigned values x and k, such that 0≤k<w, the C expression x >> k yields the value $\left \lfloor x/2^k\right \rfloor $
Two’s-complement division by a power of 2, rounding down
For C variables x and k with two’s-complement values x and unsigned value k, such that 0≤k<w, the C expression x >> k, when the shift is performed arithmetically, yields the value $\left \lfloor x/2^k\right \rfloor $
Two’s-complement division by a power of 2, rounding up
For C variables x and k with two’s-complement values x and unsigned value k, such that 0≤k<w, the C expression (x + (1 << k) - 1) >> k, when the shift is performed arithmetically, yields the value $\left \lceil x/2^k\right \rceil $
As we have seen, the “integer” arithmetic performed by computers is really a form of modular arithmetic. The finite word size used to represent numbers limits the range of possible values, and the resulting operations can overflow.
We have also seen that the two’s-complement representation provides a clever way to represent both negative and positive values, while using the same bit-level implementations as are used to perform unsigned arithmetic—operations such as addition, subtraction, multiplication, and even division have either identical or very similar bit-level behaviors, whether the operands are in unsigned or two’s complement form.
4 Floating Point
In this section, we will see how numbers are represented in the IEEE floating point format. We will also explore issues of rounding, when a number cannot be represented exactly in the format and hence must be adjusted upward or down
ward. We will then explore the mathematical properties of addition, multiplication, and relational operators.
The IEEE floating-point standard represents a number in a form V=(−1)s×M×2E
The sign s determines whether the number is negative (s=1) or positive.
The significand(尾数) M is a fractional binary number that ranges either between 1 and 2−ϵ or between 0 and 1−ϵ. (这里1−ϵ是指0.1111...12)
The exponent(阶码) E weights the value by a (possibly negative) power of 2.
The value encoded by a given bit representation can be divided into three different cases (the latter having two variants), depending on the value of exp.
The fraction field frac is interpreted as representing the fractional value f,
where 0≤f<1, having binary representation 0.fn−1...f1f0, that is, with the binary point to the left of the most significant bit. The significand is defined to be M=1+f. This is sometimes called an implied leading 1representation, because we can view M to be the number with binary representation 1.fn−1fn−2...f0. This representation is a trick for getting an additional bit of precision for free, since we can always adjust the exponent E so that significand M is in the range 1≤M<2 (assuming there is no overflow).
4.2.2 Denormalized Values
When the exponent field is all zeros, the represented number is in denormalized form. In this case, the exponent value is E=1−Bias, and the significand value is M=f.
A final category of values occurs when the exponent field is all ones. When the
fraction field is all zeros, the resulting values represent infinity, either +∞ when s=0 or −∞ when s=1. Infinity can represent results that overflow, as when we multiply two very large numbers, or when we divide by zero. When the fraction
field is nonzero, the resulting value is called a NaN, short for “not a number.” Such values are returned as the result of an operation where the result cannot be given as a real number or as infinity, as when computing ∞−∞ or −1.
Problem 8
Consider the following two 7-bit floating-point representations based on the IEEE floating-point format. Neither has a sign bit — they can only represent nonnegative numbers.
Format A
There are k=3 exponent bits. The exponent bias is 3.
There are n=4 fraction bits.
Format B
There are k=4 exponent bits. The exponent bias is 7.
There are n=3 fraction bits.
Below, you are given some bit patterns in format A, and your task is to convert
them to the closest value in format B. If necessary, you should apply the round-to even rounding rule.
All versions of C provide two different floating-point data types: float and double. On machines that support IEEE floating point, these data types correspond to single- and double-precision floating point. In addition, the machines use the round-to-even rounding mode.
Unfortunately, since the C standards do not require the machine to use IEEE floating point, there are no standard methods to change the rounding mode or to get special values such as −0,+∞,−∞,or NaN.
Most systems provide a combination of include (.h) files and procedure libraries to provide access to these features, but the details vary from one system to another. For example, the GNU compiler GCC defines program constants INFINITY (for +∞) and NAN (for NaN) when the following sequence occurs in the program file:
1 2
# define _GNU_SOURCE 1 # include<math.h>
When casting values between int, float, and double formats, the program changes the numeric values and the bit representations as follows (assuming data type int is 32 bits):
From int to float, the number cannot overflow, but it may be rounded.(float范围虽然比int大得多,但它不能精确表示所有int,因为虽然范围大,但数值之间的间隔也会越来越大)
From int or float to double, the exact numeric value can be preserved because double has both greater range (i.e., the range of representable values), as well as greater precision (i.e., the number of significant bits(有效位数)).
From double to float, the value can overflow to +∞ or −∞, since the range is smaller. Otherwise, it may be rounded, because the precision is smaller.
From float or double to int, the value will be rounded toward zero. For example, 1.999 will be converted to 1, while −1.999 will be converted to −1. Furthermore, the value may overflow. The C standards do not specify a fixed result for this case. Intel-compatible microprocessors designate the bit pattern [10 …00] (TMinw for word size w) as an integer indefinite value. Any conversion from floating point to integer that cannot assign a reasonable integer approximation yields this value. Thus, the expression (int) +1e10 yields -2147483648, generating a negative value from a positive one.
Problem 9
Assume variables x, f, and d are of type int, float, and double, respectively.
Their values are arbitrary, except that neither f nor d equals +∞,−∞,or NaN.
判断下列C表达式真假:
A. x == (int) (double) x
对的,因为double精度、范围都比int高
B. x == (int) (float) x
不对,float只有24个二进制有效位(算是默认带的1),无法精确表示所有32位整数。