深入理解计算机系统

第五章 优化程序性能

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. 进程

    信号量

  4. 两个原子操作

    P(s): 把 s-1, 且 s 不能小于 0, 否则阻塞

    V(s): 把 s+1, 之后重启任意一个阻塞的 P(s) 操作

  5. int sem_init(sem_t * s, 0, uint value) 初始化

  6. int sem_wait(sem_t * s) 测试

  7. int sem_post(sem_t * s) 增加

  8. 并行程序相当棘手,对代码看上去很小的改动可能会对性能有极大的影响

  9. 可重入函数: 不引用任何共享数据

  10. 线程安全函数: 字面意思

  11. 多个限号量或线程锁时要规定加锁顺序,避免死锁

    局部性

  12. 有良好局部性的程序通常会更快

  13. 且更容易理解和优化

Last updated

Was this helpful?