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

羽林君 2023-12-26 23:00

一、前言

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

二、优化思想

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

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 来获取 系统相关 的信息,这样可以知道我们 系统效率 的具体细节

运行结果

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

  • 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 进程分析

    • 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的统计信息

    • -D(--delay ):指定 命令的延迟统计时间,单位为 毫秒

    • -d(--detailed):显示更多细节信息。

    • -e(--event ):统计 指定事件 的数据。

    • -o(--output )输出统计信息到文件

    • -p(--pid ):对指定 pid的进程 进行统计。

    • -t(--tid ):对指定 tid的线程 进行统计。

    • -r(--repeat ):重复命令 n次 并显示 平均结果

    • -S(--sync):在开始执行前使用 sync函数

    运行结果如下:

    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 :指明要分析的性能事件。

    • -p :仅分析 pid目标进程及其创建的线程

    • -k :带符号表的内核映像所在的路径,用于注释函数。

    • -K:不显示属于内核或模块的符号。

    • -U:不显示属于用户态程序的符号。

    • -d :界面的 刷新周期,默认为 2s。因为 perf top 默认 每2smmap 的内存区域读取一次性能数据。

    • --call-graph fractal:路径上的调用率为 相对值,加起来为100%。调用顺序为 从下往上

    • perf top --call-graph graph:路径调用率为 绝对值,加起来为该函数的 热度

    运行结果如下:

    • 第一列:符号引发的 性能事件的比例,默认指 占用的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 #include #include #include #include 
    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 优化选项

    来源:https://www.jianshu.com/p/5d49eed7ffae

       ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

    推荐阅读

    【1】jetson nano开发使用的基础详细分享

    【2】Linux开发coredump文件分析实战分享

    【3】CPU中的程序是怎么运行起来的 必读

    【4】cartographer环境建立以及建图测试

    【5】设计模式之简单工厂模式、工厂模式、抽象工厂模式的对比

    本公众号全部原创干货已整理成一个目录,回复[ 资源 ]即可获得。

    羽林君 某嵌入式程序猿分享技术、生活、人生云云文字。如有诗云:去年今日此门中,人面桃花相映红。人面不知何处去,桃花依旧笑春风。
    评论
    • 数字隔离芯片是一种实现电气隔离功能的集成电路,在工业自动化、汽车电子、光伏储能与电力通信等领域的电气系统中发挥着至关重要的作用。其不仅可令高、低压系统之间相互独立,提高低压系统的抗干扰能力,同时还可确保高、低压系统之间的安全交互,使系统稳定工作,并避免操作者遭受来自高压系统的电击伤害。典型数字隔离芯片的简化原理图值得一提的是,数字隔离芯片历经多年发展,其应用范围已十分广泛,凡涉及到在高、低压系统之间进行信号传输的场景中基本都需要应用到此种芯片。那么,电气工程师在进行电路设计时到底该如何评估选择一
      华普微HOPERF 2025-01-20 16:50 73浏览
    • 日前,商务部等部门办公厅印发《手机、平板、智能手表(手环)购新补贴实施方案》明确,个人消费者购买手机、平板、智能手表(手环)3类数码产品(单件销售价格不超过6000元),可享受购新补贴。每人每类可补贴1件,每件补贴比例为减去生产、流通环节及移动运营商所有优惠后最终销售价格的15%,每件最高不超过500元。目前,京东已经做好了承接手机、平板等数码产品国补优惠的落地准备工作,未来随着各省市关于手机、平板等品类的国补开启,京东将第一时间率先上线,满足消费者的换新升级需求。为保障国补的真实有效发放,基于
      华尔街科技眼 2025-01-17 10:44 221浏览
    •     IPC-2581是基于ODB++标准、结合PCB行业特点而指定的PCB加工文件规范。    IPC-2581旨在替代CAM350格式,成为PCB加工行业的新的工业规范。    有一些免费软件,可以查看(不可修改)IPC-2581数据文件。这些软件典型用途是工艺校核。    1. Vu2581        出品:Downstream     
      电子知识打边炉 2025-01-22 11:12 49浏览
    • 本文介绍瑞芯微开发板/主板Android配置APK默认开启性能模式方法,开启性能模式后,APK的CPU使用优先级会有所提高。触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。源码修改修改源码根目录下文件device/rockchip/rk3562/package_performance.xml并添加以下内容,注意"+"号为添加内容,"com.tencent.mm"为AP
      Industio_触觉智能 2025-01-17 14:09 164浏览
    • 嘿,咱来聊聊RISC-V MCU技术哈。 这RISC-V MCU技术呢,简单来说就是基于一个叫RISC-V的指令集架构做出的微控制器技术。RISC-V这个啊,2010年的时候,是加州大学伯克利分校的研究团队弄出来的,目的就是想搞个新的、开放的指令集架构,能跟上现代计算的需要。到了2015年,专门成立了个RISC-V基金会,让这个架构更标准,也更好地推广开了。这几年啊,这个RISC-V的生态系统发展得可快了,好多公司和机构都加入了RISC-V International,还推出了不少RISC-V
      丙丁先生 2025-01-21 12:10 111浏览
    • 现在为止,我们已经完成了Purple Pi OH主板的串口调试和部分配件的连接,接下来,让我们趁热打铁,完成剩余配件的连接!注:配件连接前请断开主板所有供电,避免敏感电路损坏!1.1 耳机接口主板有一路OTMP 标准四节耳机座J6,具备进行音频输出及录音功能,接入耳机后声音将优先从耳机输出,如下图所示:1.21.2 相机接口MIPI CSI 接口如上图所示,支持OV5648 和OV8858 摄像头模组。接入摄像头模组后,使用系统相机软件打开相机拍照和录像,如下图所示:1.3 以太网接口主板有一路
      Industio_触觉智能 2025-01-20 11:04 150浏览
    • Ubuntu20.04默认情况下为root账号自动登录,本文介绍如何取消root账号自动登录,改为通过输入账号密码登录,使用触觉智能EVB3568鸿蒙开发板演示,搭载瑞芯微RK3568,四核A55处理器,主频2.0Ghz,1T算力NPU;支持OpenHarmony5.0及Linux、Android等操作系统,接口丰富,开发评估快人一步!添加新账号1、使用adduser命令来添加新用户,用户名以industio为例,系统会提示设置密码以及其他信息,您可以根据需要填写或跳过,命令如下:root@id
      Industio_触觉智能 2025-01-17 14:14 121浏览
    • 临近春节,各方社交及应酬也变得多起来了,甚至一月份就排满了各式约见。有的是关系好的专业朋友的周末“恳谈会”,基本是关于2025年经济预判的话题,以及如何稳定工作等话题;但更多的预约是来自几个客户老板及副总裁们的见面,他们为今年的经济预判与企业发展焦虑而来。在聊天过程中,我发现今年的聊天有个很有意思的“点”,挺多人尤其关心我到底是怎么成长成现在的多领域风格的,还能掌握一些经济趋势的分析能力,到底学过哪些专业、在企业管过哪些具体事情?单单就这个一个月内,我就重复了数次“为什么”,再辅以我上次写的:《
      牛言喵语 2025-01-22 17:10 41浏览
    •  万万没想到!科幻电影中的人形机器人,正在一步步走进我们人类的日常生活中来了。1月17日,乐聚将第100台全尺寸人形机器人交付北汽越野车,再次吹响了人形机器人疯狂进厂打工的号角。无独有尔,银河通用机器人作为一家成立不到两年时间的创业公司,在短短一年多时间内推出革命性的第一代产品Galbot G1,这是一款轮式、双臂、身体可折叠的人形机器人,得到了美团战投、经纬创投、IDG资本等众多投资方的认可。作为一家成立仅仅只有两年多时间的企业,智元机器人也把机器人从梦想带进了现实。2024年8月1
      刘旷 2025-01-21 11:15 390浏览
    •  光伏及击穿,都可视之为 复合的逆过程,但是,复合、光伏与击穿,不单是进程的方向相反,偏置状态也不一样,复合的工况,是正偏,光伏是零偏,击穿与漂移则是反偏,光伏的能源是外来的,而击穿消耗的是结区自身和电源的能量,漂移的载流子是 客席载流子,须借外延层才能引入,客席载流子 不受反偏PN结的空乏区阻碍,能漂不能漂,只取决于反偏PN结是否处于外延层的「射程」范围,而穿通的成因,则是因耗尽层的过度扩张,致使跟 端子、外延层或其他空乏区 碰触,当耗尽层融通,耐压 (反向阻断能力) 即告彻底丧失,
      MrCU204 2025-01-17 11:30 182浏览
    • 2024年是很平淡的一年,能保住饭碗就是万幸了,公司业绩不好,跳槽又不敢跳,还有一个原因就是老板对我们这些员工还是很好的,碍于人情也不能在公司困难时去雪上加霜。在工作其间遇到的大问题没有,小问题还是有不少,这里就举一两个来说一下。第一个就是,先看下下面的这个封装,你能猜出它的引脚间距是多少吗?这种排线座比较常规的是0.6mm间距(即排线是0.3mm间距)的,而这个规格也是我们用得最多的,所以我们按惯性思维来看的话,就会认为这个座子就是0.6mm间距的,这样往往就不会去细看规格书了,所以这次的运气
      wuliangu 2025-01-21 00:15 183浏览
    • 高速先生成员--黄刚这不马上就要过年了嘛,高速先生就不打算给大家上难度了,整一篇简单但很实用的文章给大伙瞧瞧好了。相信这个标题一出来,尤其对于PCB设计工程师来说,心就立马凉了半截。他们辛辛苦苦进行PCB的过孔设计,高速先生居然说设计多大的过孔他们不关心!另外估计这时候就跳出很多“挑刺”的粉丝了哈,因为翻看很多以往的文章,高速先生都表达了过孔孔径对高速性能的影响是很大的哦!咋滴,今天居然说孔径不关心了?别,别急哈,听高速先生在这篇文章中娓娓道来。首先还是要对各位设计工程师的设计表示肯定,毕竟像我
      一博科技 2025-01-21 16:17 100浏览
    我要评论
    0
    点击右上角,分享到朋友圈 我知道啦
    请使用浏览器分享功能 我知道啦