目录

深入理解计算机系统

第五章 优化程序性能

5.1 优化编译器的能力和局限性

- 编译器必须假设不同的指针可能指向同一位置

5.2 循环展开,每次多迭代几个元素

- 可以降低过程调用

5.4 消除循环的低效率

代码移动: 移除多次执行但结果不会改变的计算
同样是降低过程调用

5.5 减少过程调用

5.6 消除不必要的内存引用

- 也是降低过程调用
- 引入临时变量
- 编译器不能自己引入,因为内存别名的问题,即指针可能即是结果,也是过程; 读/写是相关的

5.7 指令级并行

-  实际处理器中,是同时对多指令求值的
- 代码中的数据相关会限制指令级并行

5.8 循环展开

- 优化级别高的 gcc 编译器可以帮助做到

5.9 提高并行性

5.9.1 多个累计变量

- 不再必须等前一次循环的结果
    数据很奇怪时,比如偶数都很大,奇数都很小时,可能会导致溢出

5.9.2 重新结合变换

- 编译器不会对浮点数自动进行

5.11 条件

- 使用条件传送,而不是 if
- 以避免分支预测错误,即用 'a>b?a:b'
- 条件数据传送,而非条件控制转移

5.14 性能

- 使用性能剖析程序
- 瓶颈消除以后,新的瓶颈就会出现
- 哈希函数的好坏差别很大; 质数分布

第 七 章 链接

- GNU READELF 查看目标文件的程序

静态库: 链接器只会复制用到的模块

- .a( archive )
- 使用 ar 编译生成
- 链接器解析顺序:
    集合E: 目标文件的集合
    集合U: 使用但未定义
    集合D: 已定义
    遇到库文件 f, 扫描 U, 修改 D, 随后丢弃该库文件; 所以,以后再用到该库文件时可能会报错
- 所以,静态编译是需要排好库的顺序, 库可重复出现在不同位置

动态库

- .so( shared object )
- gcc -shared -fpic
- fpic 表示生成位置无关代码( Position-Independent Code )
- 也可以运行时通过 'dlopen()' 函数加载

7.13 库打桩机制

- Library Interpositioning
- 编译时、链接时、运行时 均可打桩
编译时打桩
- gcc 的 -I 选项, 优先搜索当前文件夹的头文件, 再搜索系统目录
链接时打桩
> --wrap f 表示把 f 解析为__wrap_f 而把__real_f 解析为 f 
> gcc -wl, --wrap, malloc *.o
- -wl 参数表示把参数传递给连接器
运行时打桩
- 编译时需要源代码
- 链接时需要目标文件
- linux 下运行时指定 `LD_PRELOAD="./mylib.so" *.exe`表示动态链接先搜索这些库
- 多个库文件用逗号或空格分割
- 可以对包括系统程序在内的任何程序打桩!!
一些工具
- AR: 静态库, 插入、删除、列出和提取
- STRINGS: 列出一个目标文件所有可打印字符串
- STRIP: 从目标文件删除符号表信息
- NM: 列出一个目标文件符号表中定义的符号
- SIZE: 列出一个目标文件中节的名字和大小
- READELF: 显示一个目标文件的完整结构, 包括 ELF 头中编码的所有信息, 包括 NM 和 SIZE 的功能
- OBJDUMP: 所有二进制工具之母, 能显示一个目标文件中所有信息。最大作用是反汇编 .text 节中的二进制指令
- LINUX 系统还有 LDD: 列出运行时所有需要的共享库

第 九 章 虚拟内存

- Virtual Memory
- 方式: 按需页面调度
- 多个虚拟页面可以映射到同一个物理内存地址上
- VM 简化了链接和加载、代码和数据共享, 以及应用程序的内存分配
- 简化内存分配: 用户申请的 n 个连续虚拟内存页可以随机分散映射到物理内存
- MMU 内存管理单元, 硬件
- 虚拟页面的三种状态
    已缓存
    未缓存
    未分配
- 写时复制( Copy on Write ), fork() 函数就使用写时复制
- Linux 内存映射到文件
    1. 到普通文件, 按需进行调度
    2. 到匿名文件, 请求二进制0, 不发生磁盘与内存的数据负值, 使用时把物理内存置0即可

分配器

- C++显式分配, Java 隐式分配
- 要求
> 处理任意的申请和释放顺序
> 立刻响应请求,不允许重新排列请求
> 只使用堆
> 对齐块
> 不修改已分配块
- 吞吐量 和 内存利用率 互相牵制
- 利用率低是因为碎片
- 合理性能: 分配请求的最糟时间与空间块的数量线性相关
- 内存释放后的合并策略:
    1. 立即合并. 简单明了,但会发生抖动,即反复分割和合并
    2. 推迟合并,分配失败时,扫描整个堆,合并相邻。快速分配器的选择
- 对齐方式的 trick: `size = DSize * ( (DSize+size+Dsize- 1)/DSize )`
- 未初始化的全局变量总是被加载器初始化为0

虚拟地址与物理地址翻译

- TLB( Translation Lookaside Buffer ) 翻译后备缓存
- 页表: 保存所有虚拟地址与物理地址的映射表
- 物理地址的三层: (缓存可以分好几层)
    高速缓存
    内存
    磁盘

第 十 章 系统级IO

- 不要使用 scanf 来读取二进制文件
- 对网络套接字的 I/O 使用 RIO(书中定义的一系列函数), 以确保线程安全

限制

- 跟在输出之后的输入,中间应插入 fflsh、fseek、fsetpos、rewind 的调用
- 跟在输入后的输出,中间应插入 fseek、fsetpos、rewind 的调用
- 否则就会出现缓存区的问题
- 网络编程无法使用 lseek, 所以可以打开两个操作流,一个读,一个写,但关闭两次会出现问题
- 所以使用 RIO 函数,在书中定义

文件读写的三个层次

- 每一个进程都有一个描述符表
- 操作系统用引用计数维护文件表,记录当前位置
- v-node 表,为文件信息

第 十一 章 网络编程

> 客户端发送请求
> 服务器处理
> 服务器发送响应
> 客户端收到并处理
- 互联网可以由完全不同和不兼容的各种局域网和广域网组成
- web 服务器 80 端口
- 邮件服务器 25 端口
- /etc/services 文件包含机器提供的名字和端口的映射
- 客户端
    getaddrinfo;  socket;  connect;  riw_writen;  rio_read;  close
- 服务器
    getaddrinfo;  socket,bind,listen;  accept;  rio_read;  rio_write;  EOF;  close
- 访问服务器网页时,服务器可执行文件的 url 后可以跟程序参数,"?"分隔文件和参数,每个参数都用'&'隔开

第 十二 章 并发编程

1. I/O 多路复用,select() 函数
2. `pthread_create()`
3. 进程

信号量

- 两个原子操作
    P(s): 把 s-1, 且 s 不能小于 0, 否则阻塞
    V(s): 把 s+1, 之后重启任意一个阻塞的 P(s) 操作
- `int sem_init(sem_t * s, 0, uint value)` 初始化
- `int sem_wait(sem_t * s)` 测试
- `int sem_post(sem_t * s)` 增加
- 并行程序相当棘手,对代码看上去很小的改动可能会对性能有极大的影响
- 可重入函数: 不引用任何共享数据
- 线程安全函数: 字面意思
- 多个限号量或线程锁时要规定加锁顺序,避免死锁

局部性

- 有良好局部性的程序通常会更快
- 且更容易理解和优化