深入理解计算机系统
第五章 优化程序性能
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 内存映射到文件
到普通文件, 按需进行调度
到匿名文件, 请求二进制0, 不发生磁盘与内存的数据负值, 使用时把物理内存置0即可
分配器
C++显式分配, Java 隐式分配
要求
处理任意的申请和释放顺序 立刻响应请求,不允许重新排列请求 只使用堆 对齐块 不修改已分配块
吞吐量 和 内存利用率 互相牵制
利用率低是因为碎片
合理性能: 分配请求的最糟时间与空间块的数量线性相关
内存释放后的合并策略:
立即合并. 简单明了,但会发生抖动,即反复分割和合并
推迟合并,分配失败时,扫描整个堆,合并相邻。快速分配器的选择
对齐方式的 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 后可以跟程序参数,"?"分隔文件和参数,每个参数都用'&'隔开
第 十二 章 并发编程
I/O 多路复用,select() 函数
pthread_create()
进程
信号量
两个原子操作
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)
增加并行程序相当棘手,对代码看上去很小的改动可能会对性能有极大的影响
可重入函数: 不引用任何共享数据
线程安全函数: 字面意思
多个限号量或线程锁时要规定加锁顺序,避免死锁
局部性
有良好局部性的程序通常会更快
且更容易理解和优化
Last updated
Was this helpful?