《深入理解计算机系统》读书笔记
注:《深入理解计算机系统》是我们《系统级编程》课程的参考书。这里主要记载的是在看《深入理解计算机系统》这本书的过程中,遇到的一些以前没有注意到的知识。但是,这本书的稿子是2001年的,有点旧了,而且据说卡内基梅隆大学的讲义每年要更新30%。我会在其中增加一些我们系统级编程课程中的一些新的知识点。
第0章 绪论
1,不能用x-y<0代替x<y,因为可能会发生溢出,也不能用-y<-x来代替,因为在二进制补码表示中负数和正数的范围是不对称的。
第1章 计算机系统漫游
1,区分不同数据对象的唯一方法是通过这些数据的上下文来判断。
2,cache是由静态随机访问存储器(SRAM)实现的。L1位于处理器芯片上,而L2位于主板上,通过高速缓存总线与芯片相连。
3,进程的虚拟地址空间中,代码和数据后台紧随着的是运行时堆。代码和数据区是在进程一旦开始运行时就被制定了大小的,与此不同,作为调用像malloc和free这样的C标准库函数的结果,堆可以在运行时动态的扩展和收缩。栈也可以扩展和收缩。地址空间顶部的四分之一是为内核预留的。
第2章 信息的表示和处理
1,由于表示的精度有限,浮点运算时不可结合的,一般是选择最小的先运算。
2,字长指明整数和指针数据的大小。字长决定的最重要的系统参数是虚拟地址空间的最大大小。
3,float一般为4字节,double一般为8字节,指针一般用的是全字长,32位机上是4字节,64位机上是8字长。
4,在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节序列中最小的地址。
5,二进制代码很少能在不同机器和操作系统组合之间移植。
6,表达式~0将生成一个全1的掩码,不管机器的字大小是多少。尽管对于一个32位机器同样的掩码可以写出0xFFFFFFF,但是这样的代码是不可移植的。
7,几乎所有的编译器/机器组合都对有符号数据使用算术右移,即在左边补充符号位。
8,C和C++都支持符号和无符号数,但JAVA只支持有符号数。
9,C库中的文件<limits.h>定义了一组常量,用来限定运行编译器的这台机器的不同整形数据类型的范围,如INT_MAX,INT_MIN,UINT_MAX。
10,强制类型转换并没有改变参数的位表示,只是改变了如何将这些位解释为一个数字:
int x = -1;
unsigned ux = (unsigned)x;
这里的ux = 0xffff ffff
11,规则1:当将一个有符号数映射为它相应的无符号数时,负数就被转换成了大的正数,而非负数会保持不变。
规则2:对于小的数(<2^(w-1)),从无符号到有符号的转换将保留数字的原值,对于大的数,数字将被转换为一个负数值。
12,无符号整数加法:
x+y = x+y , x+y<2^w
x+y = x+y-2^w x+y>=2^w
13,有符号整数加法:
x+y = x+y-2^w, x+y >= 2^(w-1) 正溢出
x+y = x+y, 正常
x+y = x+y+2^w, x+y < -2^(w-1) 负溢出
14,二进制补码的非
-x = -2^(w-1), x = -2^(w-1)
-x = -x, x > 2^(w-1)
15,在单精度浮点格式(C中的float)中,s,exp和frac分别为1位,8位,23位,产生一个32位的表示。在双精度格式(C中的double)中,s,exp和frac分别为1,11位,52为,产生一个64位的表示。
16,浮点数的舍入规则是向偶数舍入(round-to-even),或者向最接近的值舍入(round-to-nearest)。
17,浮点加法不具有结合性,浮点加法满足下面的单调性属性:如果a>=b,那么对任何a和b的值,除了x不等于NaN,都有x+a>=x+b。浮点乘法也满足相应的单调性属性。
18,浮点数取非就是简单的对它的符号位去反。float f; f == -(-f)是正确的。
19,看下面这段代码
#include <iostream> #include <iomanip> using namespace std; void main() { double x = 1.3; double y = 0.4; if (x + y != 1.7) cout << "addition failed?" << endl; }
运行结果将是addition failed?" 。也就是x+y != 1.7原因就是double中保存的是近似值,而不是精确值1.7.
正确的写法应该如下:
#include <iostream> #include <iomanip> using namespace std; const double epsilon = 0.000001; bool about_equal(double x, double y) { return (x < y + epsilon) && (x > y - epsilon); } void main() { cout << "1.3 + 0.4 == 1.7: " << (1.3 + 0.4 == 1.7) << endl; cout << "about_equal(1.3 + 0.4, 1.7): " << about_equal(1.3 + 0.4, 1.7) << endl; }
第3章 程序的机器级表示
1,Linux使用了平面寻址方式(flat addressing),在这种寻址方式中,程序员将整个存储空间看作一个大的字节数组。
2,IA32中传送指令的两个操作数不能都指向存储器位置。
3,根据惯例,所有返回函数或指针值的函数都是通过将结果放在寄存器%eax中来达到目的的。
4,移位量可以是一个立即数,或者放在单字节寄存器元素%cl中。
5,函数的第一个,第二个,第三个参数分别存放在存储器中相对于%ebp中地址偏移量8,12,16的地方。
6,C中,所有的循环都会转换成do-while的形式。
7,根据惯例,寄存器%eax,%edx,%ecx被划分为调用者保存的寄存器,其余的三个(%ebx,%esi,%edi)被划分为被调用者保存的寄存器。
8,
call next next: popl %eax ;%eax中保存的是popl指令的地址
这是IA32中将程序计数器中的值放入证书寄存器的唯一方法。
9,联合(union),用关键字union来声明,允许用几种不同的类型来引用一个对象。一个联合的总的大小等于它最大域的大小。
未完待续……
第5章 优化程序性能
1,代码移动(code motion):包括识别出要执行多次(例如,在循环里)但是计算结果不会改变的计算,因而我们可以将计算机移动到代码前面的,不会被多次求值的部分。
2,消除不必要的引用,比如我们在循环中要不停的将某个值赋给一个指针,我们可以定义一个变量,现将每次求得的值赋给此变量,最后在把变量的值赋给指针。
3,超标量(superscalar):可以在每个时钟周期执行多个操作,而且是乱序的。
4,在一个IA32处理器上,所有的浮点操作都是以扩展的80位精度执行的,而浮点寄存器也是按照这个格式存储值的。只有当寄存器中的值写入存储器中时,才把它转换成32位或64位格式。
5,剖析程序(UNIX平台):在编译时加上-pg的参数,这样,在执行程序的过程中会产生一个gmon.out文件,然后执行gprof a.out 就可以了。
6,存储器别名和过程调用会严重限制编译器执行大量优化的能力。
第6章 存储器层次结构
1,如果你的程序需要的数据存储在CPU寄存器中,那么在执行期间,在零个周期内就能访问到它们。
2,SRAM将每个位存储在一个双稳态存储器单元cell里,每个单元是用一个六晶体管电路来实现的。
3,DRAM将每个位存储为对电容的充电,与SRAM不同,DRAM存储器单元对干扰非常敏感。当电容的电压被扰乱后,它就永远不会恢复了。泄漏电流的各种因素会导致DRAM单元在10---100毫秒时间内失去电荷。
4,一个d*w的DRAM总共存储了dw位信息,其中d个超单元,每个超单元有w位。
5,对扇区的访问时间有三个主要的部分:寻道时间,旋转时间和传送时间。
6,I/O桥接器连接系统总线,存储器总线和I/O总线。
7,重复引用同一个变量的程序有良好的时间局部性;对于取指令来说,循环有好的十件和空间局部性,循环体越小,循环迭代次数越多,局部性越好;对于具有步长为k的引用模式的程序,步长越小,空间局部性越好。具有步长为1的引用模式的程序有很好的空间局部性,在存储器中以大步长跳来跳去的程序空间局部性会很差。
8,高速缓存包括直接映射高速缓存(E=1),组相连高速缓存(1<E<C/B)和全相连高速缓存(E=C/B)。
第7章 链接
1,C源代码文件扮演模块的角色。任何声明带有static属性的全局变量或者函数的哦是模块私有的。类似的,任何声明为不带static属性的全局变量和函数都是公共的,可以被其他模块访问。尽可能用static属性来保护变量和函数是很好的编程习惯。
2,连接器解析多处定义的全局符号时的规则:函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。规则1:不允许有多个强符号。规则2:如果有一个强符号和多个弱符号,那么选择强符号。规则3:如果有多个弱符号,那么从这些弱符号中任意选择一个。
3,重定位就是合并输入模块,并为每个符号分配运行时地址。
4,在可执行文件中是完全链接的(已被重定位),所以它不再需要.rel节了。
5,加载:将程序拷贝到存储器并运行。
6,共享库是致力于解决静态库缺陷的新产物。共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并在存储器中和一个程序连接起来。这个过程称为动态链接,是由一个动态连接器来执行的。共享库的一个主要目的就是允许多个正在运行的进程共享存储器中相同的库代码,因而节约了存储器的资源。
附:
1,在C++中,数组下标的最大值是max(int32),如何在32位机器上突破这个限制?不能!因为C++的数组操作在汇编层会按照基地址+偏移寄存器的形式访问。32位机器上的偏移寄存器只有32位。所以,不能突破。
2,在类规模很小,但需要产生的对象数量巨大(如K,M级别)时,用new申请堆内存效率高,还是直接实例化对象的效率高?二者的上限分别取决于哪一块内存的容量?实例化效率高,这个是对程序栈直接做压栈弹栈,比堆分配要高的多。堆还要记录分配链的起始,中止地址。栈的上限取决于编译时分配的栈长度,堆取决于操作系统提供的内存大小。也就是说,栈空间要远远小于堆空间,而且系统也会使用栈,比如在掉函数时,参数的传递和返回值都是通过栈操作完成的。如果在栈上开大对象造成栈空间用完,系统要么死锁,要么退出。如果要实例化大量的小对象,最好在程序开始时申请一块大内存自己管理,然后小对象用到时,从这块大内存中做分配,也就是内存池技术,尤其是那种小对象的大小固定时,这种方法更有效。
3,数组和STL的性能比较?单纯从访问效率角度讲,数组效率高(包括时间和空间效率),但加入插入,删除,查找,排序等操作后,数组的效率就比STL低很多了。