美文网首页操作系统android之性能优化学习
《嵌入式Linux内存与性能详解》笔记4——性能优化

《嵌入式Linux内存与性能详解》笔记4——性能优化

作者: wipping的技术小栈 | 来源:发表于2020-04-05 17:36 被阅读0次

    一、前言

    前面讲了关于 内存 方面的优化,那么接下来的文章我们主要聚焦于 性能 的优化,那么主要体现在优化程序 速度 上。程序的 速度 很大程度上会影响用户体验或者程序的实际效用,所以优化 性能速度 也是程序员需要关注的一个方面,从本文起接下来的几篇文章将讲述如何对 程序性能 进行优化。

    二、优化思想

    在讲解优化之前,我们先看一下 优化的思想,它可以保证我们在学习或者优化的过程中保持对问题的关注,让我们知道是在学习什么跟如何优化。

    2.1 性能分析瓶颈分类

    说到优化,一个程序运行慢可能是有多种原因构成的,那么前辈们就将这多种原因归结为以下 3类

    • 程序的运算量很大,导致 CPU 过于繁忙。称为 CPU 瓶颈
    • 程序需要做大量的 I/O,读写文件、内存操作等等,CPU 更多的是处于等待。称为 I/O 瓶颈
    • 程序之间相互等待,结果 CPU 利用率很低,但运行速度依然很慢,事务间的共享与死锁制约了程序的性能

    2.2 优化基本原则

    • 等效原则:优化前后程序实现的功能一致。
    • 有效原则:优化后要比优化前运行速度快或占用存储空间小,或二者兼有。
    • 经济原则:优化程序要付出较小的代价,取得较好的结果。

    另外一般有 5 个基本纲领为我们提供优化思路:

    • Do it faster: 程序除了 实现功能 之外,还需要 提高其运行速度
    • Do it in parallel:能够 并行执行程序 的话,我们就可以 充分利用CPU资源,从而加速程序运行速度。
    • Do it later:对于时间紧迫的功能,有些不必要的功能,可以考虑延后执行,使程序的任务减轻,从 而腾出资源,来做重要的事。
    • Don't do it at all:设计程序时,不要加入多余无用的代码 来实现 *模块化或者提高可读性
    • Do it before:程序有可能并不需要一直工作,我们可以把一些工作在 系统空闲 时完成,这样可以减轻繁忙时程序的负担。

    2.3 优化思路

    在程序优化上,往往 20%的代码 占用了 80%的运行时间,所以我们需要找出这 20% 的代码。所以 如何找出需要优化的代码 是一个我们需要关注的问题,而这个问题我们可以有一些工具来解决,在下面的章节会提到。

    第一步:找出 性能瓶颈的代码,在笔者看来,分析程序问题,找出性能瓶颈是重中之重。
    第二步:进行 代码优化

    2.4 优化经验总结:

    下面是书中提到的一些 优化经验,在了解这些经验之前,我们需要了解几个概念:

    • 提高程序的效率:程序的 时间效率是指运行速度
    • 空间效率:是指程序 占用内存或者外存的状况
    • 全局效率 是指站在 整个系统的角度上考虑的效率
    • 局部效率 是指站在 模块或函数角度上考虑 的效率

    书中经验总结如下:

    • 不要一味地追求程序的效率,应当在满足 正确性、可靠性、健壮性、可读性 等质量因素的前提下,设法提高程序的效率。
    • 以提高程序的 全局效率为主,提高 局部效率为辅
    • 在优化程序的效率时,应当先找出限制效率的 瓶颈,不要在无关紧要之处优化。
    • 先优化 数据结构和算法,再优化 执行代码
    • 有时候 时间效率空间效率 可能对立,此时应当分析那个更重要,作出适当的折衷。例如多花费一些内存来提高性能
    • 不要追求 紧凑的代码,因为 紧凑的代码 并不能产生 高效的机器码
    • 当心那些视觉上不易分辨的操作符发生 书写错误。 比如会把 == 误写成 =,像 ||&&<=>= 这些符号也很容易发生 丢1失误。然而 编译器却不一定能自动指出这类错误
    • 变量(指针、数组) 被创建之后 应当及时把它们初始化,以 防止把未被初始化的变量当成右值使用
    • 当心变量的 初值、缺省值错误,或者精度不够
    • 当心 数据类型转换发生错误。尽量使用 显式 的数据类型转换,避免让 编译器轻悄悄地进行隐式的数据类型转换
    • 当心变量发生 上溢或下溢,数组的 下标越界
    • 当心忘记编写 错误处理程序,且要注意防止 错误处理程序本身有误
    • 当心 文件 I/O 有错误
    • 避免 编写技巧性很高代码
    • 不要设计 面面俱到、非常灵活的数据结构
    • 如果原有的代码质量比较好,尽量复用它。但是 不要修补很差劲的代码, 应当重新编写
    • 尽量使用 标准库函数,不要 发明已经存在的库函数
    • 尽量不要 使用与具体硬件或软件环境关系密切的变量
    • 把编译器的选择项设置为 严格状态
    • 如果可能的话,使用 PC-LintLogiScope 等工具进行代码审查。

    2.5 优化的层次

    在我们了解到了 程序瓶颈 之后,接下来我们就要考虑对代码进行优化:

    程序的优化,主要包括四个层次:

    1. 算法和数据结构的优化
    2. 编译器优化
    3. 代码优化
    4. 硬加速

    优化新手容易犯的错误就是:找到 程序热点 时,过于兴奋,急匆匆的就开始 局部代码 的优化。但我们需要在 更大的范围内分析,是什么导致这个 程序瓶颈 的存在,有时可能根本就不需要改动你的程序。

    三、性能评估

    一般情况下,我们可以把 性能优化 分为 2 个部分

    • 性能评估:一般而言,程序都有 二八法则20%的代码影响80%的速度。我们需要找出这20% 的代码进行优化,对其进行 瓶颈分析,找出影响速度的原因。
    • 性能优化:找出原因和需要优化的代码后,利用 编译硬件算法和数据结构 等对其进行优化。

    3.1 系统及进程性能评估

    据上面所讲,性能评测 是至关重要的一个环节,如果不知道哪里出了问题,那么优化就无从下手。程序性能的问题是有很多原因的,我们先看看一般的问题分析:

    • /proc目录下系统相关的文件
    • /proc目录下进程相关的文件

    3.1.1 proc系统相关

    系统相关属性 我们可以通过下面 2 个文件来了解。

    3.1.1.1. /proc/stat

    我们使用 cat /proc/stat 来获取 系统相关 的信息,这样可以知道我们 系统效率 的具体细节

    运行结果.png

    下面 按行 说明各行数据的意义:

    • cpuN:其数值分别代表着 CPU不同状态 下所用的时间,其单位为 jiffies(0.01s)
      1. user:从 系统启动 开始累计到 当前时刻,用户态的 CPU时间,不包含 nice值为负 的进程。 在图中的值为 19
      2. nice:从 系统启动 开始累计到 当前时刻nice值为负 的进程所占用的 CPU时间。 在图中的值为 0
      3. system:从 系统启动开始累计到 当前时刻内核 所占用的 CPU时间。 在图中的值为 166
      4. idle:从 系统启动 开始累计到 当前时刻,除 硬盘IO等待时间 的以外其它等待时间。 在图中的值为 218632
      5. iowait:从 系统启动 开始累计到 当前时刻硬盘IO 等待时间。 在图中的值为 73
      6. irq:从 系统启动 开始累计到 当前时刻硬中断时间。在图中的值为 0
      7. softirq:从 系统启动 开始累计到 当前时刻软中断时间。在图中的值为 0
      8. stealstolen:在 虚拟化环境中 运行时在 其他操作系统 上花费的时间
      9. guestLinux内核 控制下为 虚拟操作系统 运行 虚拟CPU 所花费的时间
    • intr:从 系统启动 开始累计到 当前时刻,发生的 所有中断的次数每个数 对应一个特定的 中断
    • ctxt:从 系统启动 开始累计到 当前时刻CPU 发生的 上下文交换 的次数。
    • btime:从 系统启动 开始累计到 当前时刻时间,单位为秒。
    • processes:从 系统启动 开始累计到 当前时刻 ,系统所创建的 任务数目
    • procs_running:当前 运行队列 的任务数目。
    • procs_blocked:当前 被阻塞 的任务数目。

    3.1.1.2. CPU利用率

    CPU利用率是指CPU工作时间占总时间的比重, 简单地理解为 单位时间内 CPU处于忙状态的时间占比。

    摘抄附录《CPU利用率与Load Average的区别?》中的说法:
    CPU利用率 可以看出在某一个时间段内CPU被占用的情况,如果CPU被占用时间很高,那么就需要考虑CPU是否已经处于超负荷运作,长期超负荷运作对于机器本身来说是一种损害,因此必须将CPU的利用率控制在一定的比例下,以保证机器的正常运作。

    有一种情况需要注意的是:使用 自旋锁忙等处理的锁会导致 CPU利用率的上升,此时 CPU利用率 并不能很好的反映 CPU的使用情况

    • CPU时间 = user + system + nice + idle + iowait + irq + softirq
    • CPU利用率 = 1 - (idle) / CPU时间。用于测量当前系统的 CPU负载情况
    • CPU用户态利用率 = (user+nice) / CPU时间。用于测量 用户程序CPU的占用率
    • CPU内核态利用率 = (system) / CPU时间。如果程序有大量的 系统调用,导致 Linux内核 占用了大量的 CPU,可以使用该数值来测量。
    • IO利用率 = iowait / CPU时间,测量 FLASH内存 等存储介质的 交互和等待时间

    3.1.1.3. /proc/loadavg

    运行结果.png
    loadavg 主要检查当前系统的 负载情况:下面 按从左到右 逐个说明:
    1. 1分钟 的平均负载
    2. 5分钟 的平均负载
    3. 15分钟 的平均负载
    4. 在采样时刻,运行队列的任务的数目,与 /proc/statprocs_running 表示相同意思
    5. 在采样时刻,系统中 活跃的任务个数(不包括运行已经结束的任务)
    6. 最大的 pid值,包括 轻量级进程(即线程)

    更多关于 CPU负载 的讲述可以参考 3.2.1.2小节

    3.1.2 proc进程相关

    3.1.2.1 proc进程相关属性

    要查看 进程状态 ,先需要进入 进程proc目录 下的文件夹,然后使用 cat stat 查看其状态,
    如图:

    进程stat
    下面按顺序逐个介绍各个数据的意义:
    • pid:进程号(包括线程) ,在图中值为 89
    • comm应用程序 的名字,在图中值为 sshd
    • task_state:任务的状态,,在图中值为 S。其各个状态如下:
      1. R:runnign,即 运行态
      2. S:sleeping(TASK_INTERRUPTIBLE),即 睡眠态(可被打断)
      3. D:deep sleep(TASK_UNINTERRUPTIBLE),即 睡眠态(不可被打断)
      4. T:stopped,即 停止态
      5. t:tracing stop,即 暂停态(可被继续)
      6. Z:zombie,即僵尸态
      7. X:dead,即死亡态
    • ppid父进程ID,在图中值为 1
    • pgid线程组号,在图中值为 89
    • sid:该任务所在的 会话组ID,在图中值为 89
    • tty_nr:该任务的 tty终端设备号,在图中值为 0
    • tpgid:终端的 进程组号,即当前运行在该任务所在终端的前台任务 (包括shell及应用程序) 的 PID,在图中值为 -1
    • task_flags:进程标志位,查看该任务的特性,在图中值为 4194624
    • min_flt:该任务不需要从硬盘拷数据而发生的 次缺页次数,在图中值为 107
    • cmin_flt:该任务的累计所有的 waited-for进程 曾经发生的 次缺页次数,在图中值为 364
    • maj_flt:该任务需要从硬盘拷数据而发生的 主缺页次数 ,在图中值为 0
    • cmaj_flt:该任务累计的所有的 waited-for进程 曾经发生的 主缺页次数,在图中值为 0
    • utime:该任务在 用户态 运行的时间(单位为jiffies),在图中值为 0
    • stime:该任务在 核心态 运行的时间(单位为jiffies),在图中值为 0
    • cutime:该任务累计所有的 waited-for进程 曾经在 用户态 运行的时间(单位为jiffies),在图中值为 12
    • cstime:该任务累计所有的 waited-for进程 曾经在 核心态 运行时间(单位为jiffies),在图中值为 7
    • priority:任务的 动态优先级,在图中值为 20
    • nice:任务的 静态优先级,在图中值为 0
    • num_threads:该任务所在的 线程组里的 线程个数,在图中值为 1
    • it_real_value:由于 计时间隔 导致的下一个 SIGALRM 发送进程的时延(单位为jiffies), 在图中值为 0
    • start_time:在系统启动后,到与该任务启动时的间隔(单位为jiffies), 在图中值为 247
    • vsize:该任务的 虚拟地址空间大小,在图中值为 4268032
    • rss:该任务当前 驻留物理地址空间的大小,这些页可能用于 代码、数据和栈 。在图中值为 416
    • rlim:该任务能驻留物理地址空间的 最大值 (单位为byte),在图中值为 4294967295
    • start_code:该任务在虚拟地址空间的 代码段 的起始地址,在图中值为 4648960
    • end_code:该任务在虚拟地址空间的 代码段的结束地址,在图中值为 5267368
    • start_stack:该任务在虚拟地址空间的 栈的结束地址,在图中值为 3198615200
    • kstkespsp指针(堆栈指针) 的当前值, 与在进程的内核堆栈页得到的一致,在图中值为 0
    • kstkeipip指针(指令指针)的当前值,指向将要执行的 指令指针,在图中值为 0
    • pendingsig待处理信号 的位图,记录发送给进程的普通信号,在图中值为 0
    • blocksig阻塞信号 的位图 ,在图中值为 0
    • sigignore忽略信号 的位图,在图中值为 4096
    • sigcatch被俘获信号 的位图 ,在图中值为 81925
    • wchan:如果该进程是 睡眠状态,该值给出 调度的调用点 ,在图中值为 1
    • nswap:被 swapped 的页数 (当前没用) ,在图中值为 0
    • cnswap:所有子进程被 swapped 的页数的 (当前没用),在图中值为 0
    • exit_signal:该进程结束时,向父进程所发送的信号,在图中值为 17
    • processor:运行的 CPU编号 ,在图中值为 0
    • rt_priority实时进程相对优先级别,在图中值为 0
    • task_policy:进程的调度策略,其各个值如下,在图中值为 0
      1. 0非实时进程
      2. 1FIFO实时进程
      3. 2RR实时进程
    • delayacct_blkio_ticks:累计的 块I/O延迟,以 clock tick 为单位。在图中值为 0
    • guest_time:为虚拟操作系统运行虚拟CPU所花费的时间,以 clock tick 为单位。在图中值为 0
    • cguest_time:进程的 子进程 用于用户操作系统的时间,以 clock tick 为单位。在图中值为0
    • start_data:进程的 数据段BSS段起始地址。在图中值为 5335032
    • end_data:进程的 数据段BSS段结束地址。在图中值为 5342208
    • start_brk:进程的 起始地址。在图中值为 5365760
    • arg_start:进程存放 命令行参数起始地址。在图中值为 3198615427
    • arg_end:进程存放 命令行参数结束地址。在图中值为 3198615442
    • env_start:进程存放 环境变量起始地址。在图中值为 3198615442
    • env_end:进程存放 环境变量结束地址。在图中值为 3198615533
    • exit_code:进程的 退出码。在图中值为 0

    3.1.2.2 使用proc进程相关属性计算CPU占有率

    先看看下面的公式:

    • 进程CPU占用率 = 进程占用CPU时间 / 系统总的时间
    • 进程占用 CPU 时间:可以从 进程的stat文件 获得,包括 utime、stime、cutime、cstime
    • 系统总的时间:可以通过 /proc/stat 或者 gettime函数 获得。

    想要计算 CPU占有率 还对 进程的运行采样,一般需要 2个采样点

    • 采样点1
      1. 系统时间记为 sys1
      2. 进程时间分别记为:utime1、stime1、cutime1、cstime1
    • 采样点2
      1. 系统时间记为 sys2
      2. 进程时间分别记为:utime2、stime2、cutime2、cstime2

    经过计算和采样后可以按照下面的公式来计算:

    • 进程CPU占用率 = ((utime2+stime2-cutime2-cstime2)-(utime1+stime1-cutime1-cstime1)) / (sys2-sys1)
    • 进程用户态占用率 = ((utime2-cutime2)-(utime1-cutime1)) / (sys2-sys1)
    • 进程内核态占用率 = ((stime2-cstime2)-(stime1-cstime1)) / (sys2-sys1)

    3.2 系统性能评估工具

    3.2.1 top

    top 是一个常用的 性能分析 软件,运行结果如下图所示:

    运行结果
    下面将 top 分 2 点来讲述:
    • 内存分析
    • CPU及负载分析

    3.2.1.1 内存分析

    运行结果如下图所示:

    内存
    可以看到它统计的内存有:usedfreeshrdbuffcached
    各个内存的含义在前面的 内存分析 文章中已经说过,这里不再赘述

    3.2.1.2 CPU及负载分析

    1. CPU负载

    Load AverageCPU负载 ,该数据是 每隔5秒钟 检查一次活跃的进程数,然后按特定算法计算出的数值。

    Load Average 可以理解为 CPU 的带载情况,即有多少进程需要 CPU 来处理。它并不是描述 CPU的使用情况,其本质应该是 在单位时间内,CPU正在处理的进程数以及等待CPU处理的进程数之间的和,也就是CPU进程队列的统计信息

    Load Average 越高说明越多的进程在抢占CPU,因而会导致 CPU资源的竞争越来越激烈,对于CPU资源的申请和维护的成本也会加大

    理想情况下是一个CPU带一个进程,这样就不会发生CPU资源抢占。所以在一般情况下,如果这个数除以逻辑CPU的数量,结果高于5的时候就表明系统在超负荷运转了。

    CPU利用率和CPU负载的区别

    • CPU利用率指的是 程序在运行期间实时占用的CPU百分比
    • CPU负载指的是 一段时间内正在使用和等待使用CPU的平均任务数

    两者之间并没有太大的关联。比如一个进程一直在使用CPU进行运算,那么此时CPU利用率会达到100%,但平均负载则趋近于 1。反过来说,当CPU的工作负载越大,代表CPU必须要在不同的工作之间进行频繁的工作切换。

    按照笔者的理解:

    • CPU密集型进程容易让CPU利用率升高
    • I/O 密集型进程容易导致平均负载升高
    • 频繁调度进程容易让平均负载和CPU利用率升高
    2. CPU利用率及负载
    CPU及负载
    • usr用户空间 占用CPU的百分比。
    • sy内核空间 占用CPU的百分比。
    • nic改变过优先级的进程占用CPU的百分比
    • id空闲 CPU百分比
    • ioIO等待 占用CPU的百分比
    • irq硬中断 占用CPU的百分比
    • sirq软中断 占用CPU的百分比
    • Load average:指 CPU的 负载均衡,后面的三个数分别是 1分钟5分钟15分钟的负载情况。

    2.2.1.3 进程分析

    image.png
    • PID进程ID
    • PPID父进程ID
    • USER:进程所有者
    • STAT:当前进程的状态,其值可以参考 进程stattask_state
    • VSZ:进程的 虚拟大小
    • %CPU:上次更新到现在的CPU时间占用百分比
    • COMMAND:运行的 命令行

    3.2.2 vmstat

    在嵌入式设备上,busybox 是不带有该工具,而该工具的源码也不是独立的,而是在 procps 这个中聚集中。关于 procps 的交叉编译请参考附录中的:

    • json-c 交叉编译(undefined reference to rpl_malloc )
    • 交叉编译Procps-ng-3.3.11

    编译完成后将相应的文件拷贝到设备端上就可以使用 vmstat 工具了,运行结果如下:

    运行结果
    该命令的意思是:每隔1秒运行1次vmstat,一共运行5次。

    数据含义如下:

    • procs:系统中的进程状态

      • r(running)运行队列中的进程数量。
      • b(block)在阻塞等待IO的进程数量。
    • memory:系统的内存使用情况

      • swpd使用的虚拟内存 大小。如果 swpd 的值不为0且 SI,SO的值长期为0,这种情况不会影响系统性能。
      • free空闲物理内存 大小。
      • buff:用作 文件缓冲 的内存大小。
      • cache:用作 缓存 的内存大小,如果cache值大,说明缓存的文件数多,如果频繁访问到的文件都能被cache中,那么磁盘的 读IO(bi) 会非常小。
    • swap:内存交换情况

      • si每秒从交换区写到内存的大小,由磁盘调入内存
      • so每秒写入交换区的内存大小,由内存调入磁盘

    注意: 内存够用的时候,这2个值都是 0。如果这2个值长期大于0时,系统性能会受到影响,磁盘IOCPU资源 都会被消耗。不能因为观察到 空闲内存(free) 很少或接近于0时,就认为内存不够用了。还需要结合 siso。如果 闲内存(free)很少,同时siso 也很少(大多时候是0),那么此时说明内存刚好够用,系统性能暂时不会受到影响的。

    • IO:系统IO的使用情况,如果值越大
      • bi每秒读取 的块数
      • bo每秒写入 的块数

    注意:随机磁盘读写的时候,bi** 和 bo 值越大,能看到 CPUIO等待(wa) 也会越大。**

    • system:系统情况
      • in每秒中断数,包括时钟中断。
      • cs每秒上下文切换数

    注意: 随机磁盘读写的时候,incs 值越大,能看到 CPU内核消耗的CPU时间(sy) 也会越大。

    CPUCPU 的使用情况,以 百分比表示

    • us用户进程执行时间百分比(us) 的值比较高时,说明 用户进程消耗的CPU时间多。如果该值长期超 50%,那么我们就该考虑对程序进行优化。
    • sy: 内核系统进程执行时间百分比(sy) 的值高时,说明 系统内核消耗的CPU资源多。此时系统的情况比较糟糕,我们需要排查原因并优化
    • waIO等待时间百分比(wa) 的值高时,说明 IO等待 比较严重。这可能由于 磁盘大量作随机访问造成,也有可能 磁盘出现瓶颈(块操作)
    • id空闲时间百分比

    四、查找性能瓶颈

    在分析程序的时候,往往需要找个程序的 性能瓶颈。从而对 性能瓶颈 进行优化,这样可以取得比较大的收益。我们一般都使用功能来查找 性能瓶颈,一般有 gprofOProfile 或者 perf 等。本文着重说明一下 perf 的使用方法,及如何将 perf 获取到的数据转换为 火焰图 来分析程序性能。

    4.1 perf

    4.1.1 perf编译

    perf 依赖于几种库,分别如下:

    • zlib
    • elfutils
    • binutils
      其中 binutils 就是我们常用的 readelfar 等编译器工具,一般情况下我们的 交叉编译链 都有自带,所以我们需要自行编译 zlibelfutils

    1. zlib交叉编译

    1. 点击 zlib下载地址 下载 zlib 源码。
    2. 解压并进入 zlib的目录
    3. 使用命令导出 交叉编译工具,命令举例如下:

    export CC=arm-linux-gnueabihf-gcc

    1. 使用命令进行配置,该命令会直接将编译出来的库自动加入到我们的 库路径,我们就不再需要去指定 库路径 了。如下:

    ./configure --host=arm-linux-gnueabihf --prefix=/your_compiler_path/arm-linux-gnueabihf/libc/usr

    附录

    1. 编译安装

    make -j12 && make install

    2. elfutils交叉编译

    我们编译 elfutils 是需要其里面的库 libelf。编译过程依赖 2 个工具 m4bison。在编译前请检查编译机是否有安装该工具。编译步骤如下:

    1. 点击 elfutils下载地址 下载 elfutils 源码。
    2. 解压并进入目录
    3. 使用命令进行配置:

    ./configure --host=arm-linux-gnueabihf --prefix=/your_compiler_path/arm-linux-gnueabihf/libc/usr/ --disable-debuginfod

    1. 编译安装

    make -j12 && make install

    3. perf交叉编译

    perf源码 在我们开发板上 linux 的源码中,其路径一般是 /your_linux_path/tool/perf
    编译步骤如下:

    1. 进入 perf 源码目录
    2. 使用命令进行编译

    make LDFLAGS=-static ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- DEBUG=1 HAVE_CPLUS_DEMANGLE=1

    1. 编译完成后在 perf目录下 有个叫 perf 的执行文件。将 perflibzlibelf 拷贝到开发板上对应的位置,就是 库路径和执行文件路径

    4.1.2 perf概念

    perf 的原理是通过对 系统事件 进行 统计采样 得到 统计数据,再通过分析 统计数据 得出性能瓶颈的结果。

    perf 中能采样 系统事件 分为以下 3 类:

    • Hardware Event:由 PMU部件或芯片硬件如cache等 产生,在特定的条件下探测 硬件系统事件是否发生以及发生的次数。
    • Software Event:是内核产生的事件,分布在各个功能模块中,统计和操作系统相关性能事件。比如进程切换,clock_tick数等。
    • Tracepoint Eventtracepoints 是散落在内核源码中的一些 hook函数,它们可以在特定的代码被执行到时触发,比如 slab分配器的分配次数。根据一特性,perftracepoints 产生的时间记录下来,生成报告,通过分析这些报告对性能做出准确的判断。

    4.1.2 perf使用方法

    perf 是一个工具集,一共包含 20多种 工具,下面看看常用的 5种工具

    1. perf list

    perf 的运行原理就是对 系统事件 进行采样,从而得到相关数据来分析。我们可以使用perf list 来查看编译出来的 perf 支持哪些 系统事件。每个 linux版本perf 可能有所不同。笔者的运行结果如下:

    运行结果
    可以看到一共有 10种软件事件3种硬件事件。笔者编译出来的 peff 比较简陋,所以本文主要讲述 软件事件 相关的分析。

    2. perf stat

    perf stat 可以统计 程序整体情况,它能够显示出 程序系统事件 统计数据。一般用于 分析程序的整体性能

    常用选项如下

    • -a(--all-cpus):显示 所有CPU上的统计信息
    • -C(--cpu <cpu>):显示 指定CPU的统计信息
    • -D(--delay <n>):指定 命令的延迟统计时间,单位为 毫秒
    • -d(--detailed):显示更多细节信息。
    • -e(--event <event>):统计 指定事件 的数据。
    • -o(--output <file>)输出统计信息到文件
    • -p(--pid <pid>):对指定 pid的进程 进行统计。
    • -t(--tid <tid>):对指定 tid的线程 进行统计。
    • -r(--repeat <n>):重复命令 n次 并显示 平均结果
    • -S(--sync):在开始执行前使用 sync函数

    运行结果如下:


    image.png

    perf stat 显示的默认 统计信息如下:

    • task-clock-msecs:指 CPU利用率。该值越高,说明程序花费越多的时间在 CPU计算 上而不 文件IO
    • context-switches:指 进程切换次数,记录了程序运行过程中发生 进程切换 的次数。应当尽量避免频繁的进程切换
    • cache-misses:指 **程序运行过程中的 cache非命中次数。如果该值越高,说明程序的 cache命中率越低
    • cache-references:指 **程序运行过程中的 cache命中次数。如果该值越高,说明程序的 cache命中率越高
    • CPU-migrations:表示 进程 在运行过程中发生的 CPU迁移次数
    • cycles处理器时钟周期,一条机器指令可能需要多个 cycles
    • instructions:指 程序的机器指令数目
    • IPC:指 instructions/cycles 。该值越大,说明 程序越充分利用了处理器

    3. perf top

    perf toptop 不同的是。top 常用于分析系统的整体性能。而 perf top 可以精确到系统或程序中 函数级的运行情况

    常用选项如下

    • -e <event>:指明要分析的性能事件。
    • -p <pid>:仅分析 pid目标进程及其创建的线程
    • -k <path>:带符号表的内核映像所在的路径,用于注释函数。
    • -K:不显示属于内核或模块的符号。
    • -U:不显示属于用户态程序的符号。
    • -d <n>:界面的 刷新周期,默认为 2s。因为 perf top 默认 每2smmap 的内存区域读取一次性能数据。
    • --call-graph fractal:路径上的调用率为 相对值,加起来为100%。调用顺序为 从下往上
    • perf top --call-graph graph:路径调用率为 绝对值,加起来为该函数的 热度

    运行结果如下:


    image.png
    • 第一列:符号引发的 性能事件的比例,默认指 占用的cpu周期比例
    • 第二列:符号所在的 DSO(Dynamic Shared Object),可以是 应用程序、内核、动态链接库、模块
    • 第三列DSO的类型
      [.]:表示此符号属于 用户态的ELF文件,包括 8可执行文件与动态链接库*。
      [k]:表述此符号属于内核或模块。
    • 第四列符号名。如果有些符号 不能解析为函数名,只能用 地址 表示。

    4. perf record

    perf record 可以 用于记录perf统计的数据 并输出到 文件perf.data,并使用相关工具生成 可视化 图像来分析程序。配合 perf report 可以精确的分析程序

    常用选项:

    • -e:指定记录的 系统事件
    • -a:指定记录 所有CPU的事件
    • -p:记录指定 pid进程的事件
    • -o:指定 保存数据的文件名
    • -g:指定 生成函数调用图
    • -C:记录 指定CPU的事件
    • -F:设置统计频率

    这里不展开说 perf report,我们使用另一个强有力的工具 火焰图FlameGraph,可以在 github 上找到火焰图的项目,点击下载火焰图生成软件

    下面我们通过一个例程来说明:

    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <sys/wait.h>
    #include <pthread.h>
    
    void* func_test1(void* arg)
    {
        int j = 0;
        for(int i = 0; i < 90000000; i++) 
            j=i; 
        return NULL;
    }
    
    void* func_test2(void* arg)
    {
        int j = 0;
        for(int i = 0; i < 120000000; i++) 
            j=i; 
        return NULL;        
    }
    
    int main()
    {
        pthread_t test1 = 0;
        pthread_t test2 = 0;
    
        pthread_create(&test1, NULL, func_test1, NULL);
        pthread_create(&test2, NULL, func_test2, NULL);
        pthread_join(test1,NULL);
        pthread_join(test2,NULL);
    
        return 0;
    }
    

    注意,在使用时需要注意要加参数 -g。
    步骤如下:

    1. 在设备上运行 perf record -a -g your_program
    2. 使用命令 perf script -i perf.data > perf.unfold 将生成的 perf.data 转换为 perf.unfold
    3. 将生成的 perf.unfold 拷贝到宿主机上
    4. 使用 FlameGraph 中的工具执行命令 ./stackcollapse-perf.pl perf.unfold > perf.fold
    5. 使用命令 ./flamegraph.pl perf.fold > perf.svg 生成 svg文件
    6. 使用 浏览器 打开 svg文件 (笔者使用的是chrom)

    结果如下:


    运行结果

    可以看到 func_test1func_test2 占据了 执行时间 的大头,在旁边还有 2个 函数在执行。
    点击 左侧红框 查看如下:

    左侧
    从结果中可以看到是 perf 自身的调用情况。

    再点击 右侧红框 查看如下:

    右侧
    可以看到是一个名为 swapper 的函数在执行,而这个函数是干什么用的呢?
    其实在 CPU 执行程序时,空闲的时候就会调用 swapper 函数,此时 CPU 处于 idle状态

    五、程序优化

    前面我们讲了 程序优化 的几个层次,分别是:

    1. 算法和数据结构的优化
    2. 编译器优化
    3. 代码优化
    4. 硬加速

    其中 算法和数据结构的优化 不在本文内容计划之内。使用 SIMD指令硬加速 已经在笔者的其他博客 neon指令 系列一将说到。 下面主要简单地讲述一下 编译器优化代码优化

    5.1 编译器优化

    编译器按照 arm-linux-gcc 系列来说明。下面将按书中所写,列举出 gcc 的相关编译优化选项。如果你想 关掉 某一个优化选项, 你可以在 -f优化选项 之间增加 no
    在编译时,我们一般回加入 -O1、-O2和-O3 等选项,其实它们的作用就是让编译器进行不同程度的优化,那么每种优化打开的编译选项都不同,下面看看每种优化都打开了哪些选项。

    5.1.1 -O1优化

    • -fcprop-registers:
      复写传播,一般情况下该选项会与 常量折叠 一起出现。因为在函数中把寄存器分配给变量,所以编译器执行二次检查以便 减少调度依赖性 并且 删除不必要的寄存器复制操作。以下面的代码作说明。
    const int i = 2*2;
    

    编译器确实将 2*2 算成 4 了,以后碰到 i 就用 4 替换,这个计算 2*2 的过程据说叫 常量折叠(const folding),而用 4 替换 i 的过程叫做 复写传播(copy propagation)

    • -fdefer-pop:
      该优化选项与 函数返回 有关。 一般情况下,函数返回,输入参数被立即弹出堆栈后。但该优化选项会 推迟弹出函数调用的输入参数,等必要时与几个函数调用参数一起弹出。这样可以 节省处理时间,但也 会使堆栈中的内容有些杂乱

    • -fdelayed-branch:
      该选项会让编译器 试图根据指令周期时间重新安排指令, 把尽可能多的指令 移动到条件分支前,以便充分的利用处理器的 缓存

    • -fguess-branch-probability:
      该选项直译为 猜测分支可能性,该选项试图让编译器 确定条件分支可能的结果,并且移动相应的指令。这有可能导致不同的编译器会编译出迥然不同的目标代码。

    • -fif-conversion:
      if-then 语句应该是程序中仅次于循环的最消耗时间的语句。简单的if-then语句可能在最终的 汇编代码 中产生 较多条件分支。 通过 减少或者删除条件分支 ,以及使用 条件传送设置标志运算技巧 来替换 if-then语句。由此编译器可以减少 if-then语句 花费的时间量。

    • -fif-conversion2:
      相比于 -fif-conversion-fif-conversion2 加入更高级的 数学特性, 减少实现 if-then语句 所需的 条件分支

    • -floop-optimize:
      优化循环 通常可以很大程度地 提高程序性能。一般情况下,程序会存在 大型且复杂的循环。通过删除在循环内没有改变值的变量赋值操作,可以 减少循环内执行指令的数量,在很大程度上提高性能。 同时也优化那些确定何时离开循环的条件分支, 以便减少分支的影响。

    • -fmerge-constants:
      尝试(string constants and floating point constants)
      该选项会让编译器试图 横跨编译单元合并同样的常量。这一特性有时候会导致 很长的编译时间

    5.1.2 -O2优化

    • -falign-functions:
      这个选项用于使 函数对准内存中特定边界的开始位置。大多数处理器按照页面读取内存,并且确保全部函数代码位于单一内存页面内。这样 同一函数就不需要读取到多个内存页中

    • -fcaller-saves:
      该选项可以让编译器对 函数调用 只进行一次 寄存器的保存和恢复操作,而不是在每个函数调用中都进行。 如果调用多个函数, 这样能够节省时间。

    • -fcrossjumping:
      该选项让编译器 跨越跳转的转换代码进行处理, 以便组合分散在程序各处的相同代码。 这样可以 减少代码的长度, 但是有可能不会对程序性能产生直接影响。

    • -fcse-follow-jumps:
      CES即通用子表达式消除技术(common subexpression elimination)。在程序中,有时候会有一些 无法到达的代码,该选项让编译器查找程序中的此类代码,然后跳过该代码的 跳转指令。最常见的就是 if-elseelse部分

    • -fdelete-null-pointer-checks:
      该选项让编译器扫描 汇编语言代码,检查可能存在 空指针的代码。 编译器假设间接引用空指针将 停止程序

    • -fexpensive-optimizations:
      该选项让编译器执行 代价高昂的各种优化技术(编译时的角度)。但是不一定保证运行时性能能提升,有可能对运行时的性能产生负面影响。

    • -fforce-mem:
      该选项让编译器在做算术操作前,强制将内存数据copy到寄存器 中以后再执行。对于只涉及 单一指令的变量来说,这样也许不会有很大的优化效果。但是对于 很多指令中都涉及到的变量(比如数学操作) 来说,这时有显著的优化。因为和访问内存中的值相比,处理器访问寄存器中的值要快的多。
      该选项有可能导致 内存与寄存器之间的数据不一致。对于某些依赖 内存操作顺序而进行的逻辑,需要做 严格的处理 后才能进行优化。例如采用 volatile关键字限制变量的操作方式或者 利用barrier迫使cpu严格按照指令序执行

    • -fgcse:
      该选项让编译器 所有汇编代码执行全局通用表达式消除。该操作 试图分析汇编代码并且结合通用片段,消除冗余的代码段。如果代码使用 计算性的goto,推荐 使用-fno-gcse选项关闭该优化选项

    • -foptimize-sibling-calls:
      该选项可以优化 尾递归的调用。一般情况下 递归的函数 可以被 展开为一系列一般的指令, 而不是使用分支。 这样处理器的指令缓存能够加载展开的指令并且处理他们。和 需要分支操作的函数相比这样更快。

    • -fregmove:
      该选项让编译器试图 重新分配 mov 指令中使用的寄存器,并且将其作为 其他指令的操作数,以便 最大化捆绑的寄存器的数量。说实在的,笔者目前也不清楚书中所讲关于该选项的内容,有知道的读者还请不吝赐教

    • -freorder-functions:
      该选项让编译器重新安排指令块以便改进 分支操作代码局部性

    • -frerun-cse-after-loop:
      该选项让编译器在对 任何循环 已经进行过优化之后执行 运行通用子表达式消除。这样确保在展开循环代码之后更进一步地优化代码

    • -fsched-interblock:
      该选项让编译器能够 跨越指令块调度指令。 这可以非常灵活地 移动指令以便等待期间完成的工作最大化

    • -fschedule-insns:
      该选项让编译器将 重新安排指令,以让等待数据的处理器可以执行其他操作。 比如对于在进行浮点运算时有延迟的处理器来说, 这使处理器在等待浮点结果时可以加载其他指令

    • -fstrength-reduce:
      该选项让编译器对循环执行优化并且 删除迭代变量迭代变量 是 捆绑到循环计数器的变量,类似于for循环中常用的 i,j

    • -fstrict-aliasing:
      该选项让编译器强制实行高级语言的严格变量规则,确保不在数据类型之间共享变量。比如 intfloat 不使用相同的内存区域。

    • -fthread-jumps:
      在某些情况下, 一条跳转指令可能转移到另一条分支语句。 通过一连串跳转,编译器确定多个跳转之间的终目 标并且把第一个跳转重新定向到终目标。 也就是直接优化从第一个跳转到最后一个目标代码

    • -funit-at-a-time:
      该选项让编译器在优化程序之前读取 整个汇编代码。 这使编译器可以 重新安排不消耗大量时间的代码以便优化指令缓存。 缺点是编译时 花费相当多的内存,对于 小型计算机可能是一个问题

    5.1.3 -O3优化

    • -fgcse-after-reload:
      完全重新加载生成的且优化后的汇编语言代码之后执行第二次gcse优化,帮助消除不同优化代码方式创建的任何冗余段.

    • -finline-functions:
      该选项让编译器 不为函数创建单独的汇编语言代码, 而是把函数代码包含在调度程序的代码中。按照笔者理解将多个函数集成到一个函数中。该优化选项可用充分的利用指令缓存,而不是在每次函数调用时进行分支操作,由此提高性能。

    5.2 代码优化

    按照笔者理解,代码优化 更加考验一个人的经验积累。本节不会详细介绍书中每种 代码优化技巧 的原理,主要是总结 代码优化技巧结论。可以快速应用于 开发 中。

    • 数据类型:针对 8位和16位ARM 是在将数据加载到寄存器中,完成 符号位扩展。数据加载指令 ldr、ldrsb、ldrb、 lsdh、lsdsh、ldr等,它们的 指令周期是一样的。所以 char、short、int 其效率是相同的。

    • if与switch:如果 case值 变化不大(太大的话,会导致跳转表过大,造成内存的浪费),有足够的分支(大 于 4 个分支)switch 会带来性能极大的提升。

    • 循环

      1. 展开循环分支,降低循环的次数,减少 分支语句 对循环的影响。
      2. 合并循环,既可以 提高程序性能并减少代码尺寸,同时不影响功能。
      3. 流水线级数越高,对分支操作越加敏感。在程序正常运行期间处理当前代码时,预取模块读取并解码 下一部分指令而 不使存储总线闲置下来。如果遇到 分支语句,则会 清除流水线而立刻使预先进行的读取和解码工作无效。流水线 级数越多 ,系统就花费越多的时间 来填充流水线。在这期间 CPU会闲置下来。因此,分支语句越少,性能越高。
    • 数组:尽量使用数组的大小是 4或8 的倍数,用此倍数展开循环体 一维数组和多维数组 。

    • 函数

      1. 函数的参数最好 不多于 4 个。
      2. 尽量限制函数内部循环所用局部变量的数目,最好 不超过12个,以便编译器能把 变量分配到寄存器。
      3. 如果不需要 返回值,则尽量定义为 void,这样可以 节省 一个 寄存器R0。 如果需要返回值,则最好 限定在4个字节,使用一个 寄存器R0 即可。

    六、参考链接

    /proc/stat 详解
    Linux top命令详解
    Linux CPU性能优化方法
    json-c 交叉编译(undefined reference to rpl_malloc )
    交叉编译Procps-ng-3.3.11【转】
    CPU 利用率背后的真相,你知道吗?
    CPU利用率与Load Average的区别?
    死锁一定会造成cpu使用率飙升吗?
    深入理解perf报告中的swapper进程
    系统级性能分析工具perf的介绍与使用
    perf工具+ 火焰图分析性能
    perf 工具原理与使用
    GNU下优化代码
    gcc -o 优化选项

    相关文章

      网友评论

        本文标题:《嵌入式Linux内存与性能详解》笔记4——性能优化

        本文链接:https://www.haomeiwen.com/subject/hwuhyhtx.html