计算机系统课程 笔记总结 CSAPP第五章 优化程序性能(5.1-5.14)

目录

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

5.2 表示程序性能

5.3 程序示例 

5.4 消除循环的低效率

5.5 减少过程调用

5.6 消除不必要的内存引用

5.7 理解现代处理器

5.7.2 功能单元的性能

5.7.3 处理器操作的抽象模型

5.8 循环展开

扫描二维码关注公众号,回复: 9190370 查看本文章

 

5.9 提高并行性

5.9.2 重新结合变换

5.10 优化合并代码的结果小结

5.11 一些限制因素

5.11.1 寄存器溢出

5.11.2 分支预测和预测错误惩罚

5.11.2.1 不要过分关心可预测的分支

5.11.2.2 书写适合用条件传送实现的代码

5.12 理解存储性能 

5.12.1 加载的性能

5.12.2 存储的性能

5.13 应用:性能提高技术

5.14 确认和消除性能瓶颈

5.14.1 程序剖析

5.14.2 使用剖析程序来指导优化


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

  • 更可靠(各种条件下的正确性、安全性)
  • 可移植
  • 更强大(功能)
  • 更方便(安装、使用、帮助/导航、可维护)
  • 更规范(格式符合编程规范、接口规范 )
  • 更易懂(能读明白、有注释、模块化—清晰简洁)
  • 更正确(本课程重点!各种条件下)
  • 更省(存储空间、运行空间)
  • 更美(UI 交互)
  • 更快(本课程重点!本章重点!)

 

对优化的控制:指定优化级别

  • -O1 (普通)
  • -O2 (被接受)
  • -O3

优化可能会使语言和编码风格变得混乱降低程序的可读性和模块性,程序易出错难以修改和扩展

 

  • 两个指针可能指向同一个内存位置的情况称为内存别名使用
  • 在只执行安全的优化中,编译器必须假设不同的指针可能指向内存中同一个位置

 

  • 内联函数替换
    • 将函数调用替换为函数体

5.2 表示程序性能

  • 每元素的周期数(CPE)
    • 表示程序性能
    • eg.
      • 图像的像素
      • 矩阵中的元素
  • 4GHz
    • 表示处理器时钟运行频率为每秒4×109个周期
  • 时钟周期
    • 度量值表示执行了多少条指令

 

计算前置和

 

第二个函数使用:循环展开

  • 每次迭代两个元素

 

 

  • 性能比较:
    • 斜率,表示每元素的周期数(CPE)的值

5.3 程序示例 


5.4 消除循环的低效率

  • 代码移动
    • 减少计算执行的频率
      • 如果它总是产生相同的结果
      • 将代码从循环中移出
      • eg. strlen() 移除循环

 

复杂运算简化

  • 用更简单的方法替换昂贵的操作
    • 移位、加,替代乘法/除法
      • 16*x        -->        x << 4
    • 实际效果依赖于机器
    • 取决于乘法或除法指令的成本
      • Intel Nehalem CPU整数乘需要3个CPU周期
  • 识别乘积的顺序(Recognize sequence of products)
    • 识别产品(编译生成对的机器程序)的顺序

 

共享公用子表达式

  • 重用表达式的一部分
  • GCC 使用 –O1 选项实现这个优化

5.5 减少过程调用

函数调用

  • 程序行为中严重依赖执行环境的方面,程序员要编写容易优化的代码,以帮助编译器。
  • 将字符串转换为小写的函数
  • 平方级别的性能

  • 提高性能
    • 把调用 strlen 移到循环外
    • 根据:从一次迭代到另一次迭代时结果不会变化
    • ——代码移动的形式
  • 为什么编译器不能将strlen从内层循环中移出呢?
    • 函数可能有副作用
      • 例如:每次被调用都改变全局变量/状态
    • 对于给定的参数,函数可能返回不同的值
      • 依赖于全局状态/变量的其他部分
      • 函数lower可能与 strlen 相互作用
    • Warning:
      • 编译器将函数调用视为黑盒
      • 在函数附近进行弱优化
    • 补救措施:
      • 使用 inline 内联函数
        • 用 –O1 时GCC这样做,但局限于单一文件之内
      • 程序员自己做代码移动

5.6 消除不必要的内存引用

数值需要从内存中读出,再写入,浪费内存引用时间

  • 内存别名使用
    • 两个不同的内存引用指向相同的位置
    • C很容易发生
      • 因为允许做地址运算
      • 直接访问存储结构
    • 编译器不知道函数什么时候被调用,会不会在别处修改了内存,特别是并行化后,改变顺序的优化等。
    • 编译器保守的方法是不断的读和写内存,即使这样效率不高
    • 养成引入局部变量的习惯
      • 在循环中累积——用寄存器别名替换
        • 用一个局部变量计算后再引用内存
      • 告诉编译器不要检查内存别名使用的方法

5.7 理解现代处理器

现代CPU设计——流水线

利用指令级并行

  • 需要理解现代处理器的设计
    • 硬件可以并行执行多个指令
  • 性能受数据依赖的限制
  • 简单的转换可以带来显著的性能改进
    • 编译器通常无法进行这些转换
    • 浮点运算缺乏结合性和可分配性

现代CPU设计——超标量

超标量处理器

定义:一个周期执行多条指令. 这些指令是从一个连续的指令流获取的,通常被动态调度的.

5.7.2 功能单元的性能

  • 延迟
    • 表示完成运算所需要的总时间
  • 发射时间
    • 表示两个连续的同类型的运算之间需要的最小的时钟周期数
  • 容量
    • 表示能够执行该运算的功能单元数量
  •  
  • 延迟界限>=吞吐量界限
  • 延迟界限
    • 按照严格顺序完成合并运算的函数所需要的最小CPE值
  • 吞吐量界限
    • CPE最小界限
  •  
  • 计算n个元素的乘积/和
    • 需要大约 L×n+K 个时钟周期
    • L:合并运算的延迟;K:调用函数和初始化以及终止循环的开销
    • 此时CPE=L

5.7.3 处理器操作的抽象模型

  • 关键路径 critical path
    • 指明执行该程序所需时间的一个基本下界

5.8 循环展开

  • 增加每次迭代计算的元素的数量
  • 减少循环迭代次数
  • 优化性能
    • 减少了不直接有助于程序结果的操作的数量
      • 例如:循环索引计算、条件分支
    • 减少整个计算中关键路径上的操作数量
  • 基础优化
    • 把函数vec_length移到循环外
    • 避免每个循环的边界检查
    • 用临时/局部变量累积结果

 

5.9 提高并行性

  • 计算 (长度=8)
    •  ((((((((1 * d[0]) * d[1]) * d[2]) * d[3]) * d[4]) * d[5]) * d[6]) * d[7])
  • 顺序依赖性Sequential dependence
    • 性能: 由OP的延迟决定

5.9.2 重新结合变换


5.10 优化合并代码的结果小结


5.11 一些限制因素

5.11.1 寄存器溢出

 

  • 并行度p超过可用的寄存器数量
    • 编译器溢出
    • 将某些临时值存放到内存中,通常是在运行时堆栈上分配空间
  • “维护多个累计变量的优势”就很可能消失
  • x86-64有足够多的寄存器,大多数循环在出现寄存器溢出之前就将达到吞吐量限制

 

5.11.2 分支预测和预测错误惩罚

 

 

  • 当遇到条件分支时,无法确定继续取指的位置
    • 选择分支:将控制转移到分支目标
    • 不选择分支:继续下一个指令
  •  直到分支/整数单元的结果确定后才能解决

 

分支预测

  • 猜测会走哪个分支
  • 在预测的位置开始执行指令
    • 但不要真修改寄存器或内存数据

 

 

5.11.2.1 不要过分关心可预测的分支

5.11.2.2 书写适合用条件传送实现的代码


5.12 理解存储性能 

  • 加载:从内存读到寄存器
  • 存储:从寄存器写到内存

5.12.1 加载的性能

  • 一个包含加载操作的程序的性能既依赖于
    • 流水线的能力
    • 加载单元的延迟
  • 对于两个加载单元而言
    • 其每个时钟周期只能启动一条加载操作
    • CPE不可能小于0.50
  • 对于每个被计算的元素必须加载k个值的应用
    • CPE不可能小于 k/2

5.12.2 存储的性能

  • 每个时钟周期开始一条存储
  • 存储操作不影响寄存器
    • 存储操作不会产生数据相关

 

  • 存储单元
    • 包含一个存储缓冲区
      • 包含已经被发射到存储单元而又还没有完成的存储操作的地址和数据
      • “完成”包括更新数据高速缓存
      • 使得一系列存储操作不必等待每个操作都更新高速缓存就能够执行

5.13 应用:性能提高技术

  • 高级设计
    • 选择适当的算法和数据结构
  • 基本编码原则
    • 避免限制优化的因素
      • 消除连续的函数调用
        • 尽可能将计算移到循环外
        • 妥协程序的模块性
      • 消除不必要的内存引用
        • 引入临时变量来保存中间结果
        • 只用在最后的值计算出来时,才将结果存放到数组和全局变量
  • 低级优化
    • 结构化代码以利用硬件功能
      • 展开循环,降低开销
      • 使用累积变量和重新结合,提高指令级并行
      • 用功能性的风格重写条件操作,使得编译采用条件数据传送

5.14 确认和消除性能瓶颈

  • 代码剖析程序 code profiler
  • Amdahl定律

 

5.14.1 程序剖析

 

  • gcc -Og -pg xxx.c -o xxx
  • ./xxx file.txt
  • gprof xxx
    • 产生文件gmon.out
    • 列出执行各个函数花费的时间

 

5.14.2 使用剖析程序来指导优化

发布了36 篇原创文章 · 获赞 11 · 访问量 3512

猜你喜欢

转载自blog.csdn.net/gzn00417/article/details/104236361