速度优化:重新认识速度优化

作者:helson赵子健

应用的速度优化是我们使用最频繁,也是应用最重要的优化之一,它包括启动速度优化,页面打开速度优化,功能或业务执行速度优化等等,能够直接提升应用的用户体验。因此,只要是 Android 开发者,肯定或多或少有过速度相关的优化经验。但是大部分人谈到速度优化,只能想到一些零碎的优化点,比如使用多线程、预加载等等。这对速度的提升肯定是不够的,想要做得更好,我们不妨来思考下面几个问题:

  • 我们的优化方案是全面且体系的吗?
  • 我们的方案为什么能提升速度呢?
  • 我们的方案效果怎样?

想要回答好这几个问题,我们就需要了解影响和决定应用速度的底层原理及本质。那从底层来看,CPU、缓存、任务调度才是决定应用速度最本质的因素。CPU 和缓存都属于硬件层,任务调度机制则属于操作系统层。

那这一节课,我们就一起深入硬件和操作系统层面去了解以上三个因素是如何决定应用速度的,重新认识应用的速度优化,由下而上地建立起速度优化的认知体系和方法。

如何从 CPU 层面进行速度优化?

我们知道,所有的程序最终会被编译成机器码指令,然后交给 CPU 执行,CPU 以流水线的形式一条一条执行程序的机器码指令。当我们想要提升某些场景(如启动、打开页面、滑动等)的速度时,本质上就是降低 CPU 执行完这些场景指令的时间,这个时间简称为 CPU 时间。想要降低 CPU 时间,我们需要先知道程序所消耗 CPU 时间的计算公式:CPU 时间=程序的指令数 x 时钟周期时间 x 每条指令的平均时钟周期数。下面一一解释一下这三项因子的含义。

  • 程序的指令数:这一项很好理解,就是程序编译成机器码指令后的指令数量。

  • 时钟周期时间:每一次时钟周期内,CPU 仅完成一次执行,所以时钟周期时间越短,CPU 执行得越快。或许你对时钟周期时间不熟悉,但是它的倒数也就是时钟周期频率,你肯定听说过。1 纳秒的时钟周期时间就是 1 GHZ 的时钟周期频率,厂商发布新手机或者我们购买新手机时,都或多或少会提到 CPU 的时钟频率,比如高通骁龙 888 这款 CPU 的时钟频率是 2.8 GHZ,这个指标也是衡量 CPU 性能最重要的一个指标

  • 每条指令的平均时间周期:是指令执行完毕所消耗的平均时间周期,指令不同所需的机器周期数也不同。对于一些简单的单字节指令,在取指令周期中,指令取出到指令寄存器后会立即译码执行,不再需要其它的机器周期。对于一些比较复杂的指令,例如转移指令、乘法指令,则需要两个或者两个以上的机器周期。

从 CPU 来看,当我们想要提升程序的速度时,优化这三项因子中的任何一项都可以达到目的。那基于这三项因子有哪些通用方案可以借鉴呢?

减少程序的指令数

通过减少程序的指令数来提升速度,是我们最常用也是优化方案最多的方式,比如下面这些方案都是通过减少指令数来提升速度的。

  1. 利用手机的多核:当我们将要提速的场景的程序指令交给多个 CPU 同时执行时,对于单个 CPU 来说,需要执行的指令数就变少了,那 CPU 时间自然就降低了,也就是并发的思想。但要注意的是,并发只有在多核下才能实现,如果只有一个 CPU,即使我们将场景的指令拆分成多份,对于这个 CPU 来说,程序的指令数依然没有变少。如何才能发挥机器的多核呢?使用多线程即可,如果我们的手机是 4 核的,就能同时并发的运行 4 个线程。

  2. 更简洁的代码逻辑和更优的算法:这一点很好理解,同样的功能用更简洁或更优的代码来实现,指令数也会减少,指令数少了程序的速度自然也就快了。具体落地这一类优化时,我们可以用抓 trace 或者在函数前后统计耗时的方式去分析耗时,将这些耗时久的方法用更优的方式实现。

  3. 减少 CPU 的闲置:通过在 CPU 闲置的时候,执行预创建 View,预准备数据等预加载逻辑,也是减少指令数的一种优化方案,我们需要加速场景的指令数量由于预加载执行了一部分而变少了,自然也就快了。

  4. 通过其他设备来减少当前设备程序的指令数:这一点也衍生很多优化方案,比如 Google 商店会把某些设备中程序的机器码上传,这样其他用户下载这个程序时,便不需要自己的设备再进行编译操作,因为提升了安装或者启动速度。再比如在打开一些 WebView 网页时,服务端会通过预渲染处理,将 IO 数据都处理完成,直接展示给用户一个静态页面,这样就能极大提高页面打开速度。

上面提到的这些方案都是我们最常用的方案,基于指令数这一基本原理,还能衍生出很多方案来提升速度,这里没法一一列全,大家也可以自己想一想还能扩展出哪些方案出来。

降低时钟周期时间

想要降低手机的时钟周期,一般只能通过升级 CPU 做到,每次新出一款 CPU,相比上一代,不仅在时钟周期时间上有优化,每个周期内可执行的指令也都会有优化。比如高通骁龙 888 这款 CPU 的大核时钟周期频率为 2.84GHz,而最新的 Gen 2 这款 CPU 则达到了 3.50GHz。

虽然我们没法降低设备的时钟周期,但是应该避免设备提高时钟周期时间,也就是降频现象,当手机发热发烫时,CPU 往往都会通过降频来减少设备的发热现象,具体的方式就是通过合理的线程使用或者代码逻辑优化,来减少程序长时间超负荷的使用 CPU。

降低每条指令的平均时间周期

在降低每条指令的平均时间周期上,我们能做的其实也不多,因为它和 CPU 的性能有很大的关系,但除了 CPU 的性能,以下几个方面也会影响到指令的时间周期。

  1. 编程语言:Java 翻译成机器码后有更多的简介调用,所以比 C++ 代码编译成的机器码指令的平均时间周期更长。

  2. 编译程序:一个好的编译程序可以通过优化指令来降低程序指令的平均时间周期。

  3. 降低 IO 等待:从严格意义来说,IO 等待的时间并不能算到指令执行的耗时中,因为 CPU 在等待 IO 时会休眠或者去执行其他任务。但是等待 IO 会使执行完指令的时间变长,所以这里依然把减少 IO 等待算入是降低每条指令的平均时间周期的优化方案之一。

如何从缓存层面进行速度优化?

程序的指令并不是直接就能被 CPU 执行的,而是要放在缓存中,CPU 从缓存中读取,而且一个程序也不可能全是 CPU 计算逻辑,必然也会涉及到 IO 的操作或等待,比如往磁盘或者内存中读写数据成功后才能继续执行后面的逻辑,所以缓存也是决定应用速度的关键因素之一。缓存对程序速度的影响主要体现在 2 个方面:

  1. 缓存的读写速度;
  2. 缓存的命中率。

下面就详细讲解一下这 2 方面对速度的影响。

缓存的读写速度

手机或电脑的存储设备都被组织成了一个存储器层次结构,在这个层次结构中,从上至下,设备的访问速度越来越慢,但容量也越来越大,并且每字节的造价也越来越便宜。寄存器文件在层次结构中位于最顶部,也就是第 0 级。下图展示的是三层高速缓存的存储结构。

高速缓存是属于 CPU 的组成部分,并且实际有几层高速缓存也是由 CPU 决定的。以下图高通骁龙 888 的芯片为例,它是 8 块核组成的 CPU,从架构图上可以看到,它的 L2 是 1M 大小(没有 L1 是因为这其实只是序号称呼上的不同而已,你也可以理解成 L1),L3 是 3M 大小,并且所有核共享。

不同层之间的读写速度差距是很大的,所以为了能提高场景的速度,我们需要将和核心场景相关的资源(代码、数据等)尽量存储在靠上层的存储器中。 基于这一原理,便能衍生出了非常多的优化方案,比如常用的加载图片的框架 Fresco,请求网络的框架 OkHttp 等等,都会想尽办法将数据缓存在内存中,其次是磁盘中,以此来提高速度。

缓存的命中率

将数据放在缓存中是一种非常入门的优化思想,也是非常容易办到的,即使是开发新手都能想到以此来提升速度。但是我们的缓存容量是有限的,越上层的缓存虽然访问越快,但是容量越少,价格也越贵,所以我们只能将有限的数据存放在缓存中,在这样的制约下,提升缓存的命中率往往是一件非常难的事情

一个好的编译器可以提升寄存器的命中率,好的操作系统可以提升高速缓存的命中率,对于我们应用来说,好的优化方案可以提升主存和硬盘的命中率,比如我们常用的 LruCache 等数据结构都是用来提升主存命中率的。除了提升应用的主存,应用也可以提升高速缓存的命中率,只是能做的事情不多,后面的章节中也会介绍如何通过 Dex 中 class 文件重排,来提升高速缓存读取类文件时的命中率。

想要提高缓存命中率,一般都是利用局部性原理(局部性原理指如果某数据被访问,则不久之后该数据可能再次被访问,或者程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问)或者通过行为预测,分析大概率事件等多种原理来提高缓存命中率。

如何从任务调度层面进行速度优化?

我们学过操作系统为了能同时运行多个程序,所以诞生了虚拟内存这个技术,但只有虚拟内存技术是不够的,还需要任务调度机制,所以任务调度也属于操作系统关键的组成之一。有了任务调度机制,我们的程序才能获得 CPU 的资源并正常跑起来,所以任务调度也是影响程序速度的本质因素之一

我们从两个方面来熟悉任务调度机制,一是调度机制的原理,二是任务的载体,即进程的生命周期。

在 Linux 系统中,任务调度的维度是进程,Java 线程也属于轻量级的进程,所以线程也是遵循 Linux 系统的任务调度规则的,那进程的调度规则又是怎样的呢?Linux 系统将进程分为了实时进程和普通进程这两类,实时进程需要响应技术的进程,比如 UI 交互进程,而普通进程对响应速度要求不是非常高,比如读写文件、下载等进程。两种类型的进程的调度规则也不一样,我们分别来说。

首先是实时进程的调度规则。Linux 系统对实时进程的调度策略有两种:先进先出(SCHED_FIFO)和循环(SCHED_RR)。Android 只使用了 SCHED_FIFO 这一策略,所以我们主要介绍 SCHED_FIFO 。当系统使用先进先出的策略来调度进程时,如果某个进程占有 CPU 时间片,此时没有更高优先级的实时进程抢占 CPU,或该进程主动让出,那么该进程就始终保持使用 CPU 的状态。这种策略会提高进程运行的持续时间,减少被打断或被切换的次数,所以响应更及时。Android 中的 AudIO、SurfaceFlinger、Zygote 等系统核心进程都是实时进程。

非实时进程也称为普通进程,针对普通进程,Linux 系统则采用了一种完全公平调度算法来实现对进程的切换调度,我们可以不需要知道这一算法的实现细节,但需要了解它的原理。在完全公平调度算法中,进程的优先级由 nice 值表示,nice 值越低代表优先级越大,但是调度器并不是直接根据 nice 值的大小作为优先级来进行任务调度的,当每次进程的时间片执行完后,调度器就会寻找所有进程中运行时间最少的进程来执行

既然调度器是根据进程的运行时间来进行任务调度,那进程优先级即 nice 值的作用又体现在哪呢?实际上,这里进程的运行时间并不是真实的物理运行时间,而是进行了加权计算的虚拟时间,这个权值系数就是 nice 值,所以同样的物理时间内,nice 值越低的进程所记录的运行时间实际越少,运行时间更少就更容易被调度器所选择,优先级也就这样表现出来了。在 Android 中,除了部分核心进程,其他大部分都是普通进程。

了解了进程的调度原理,我们再来了解一下进程的生命周期。

通过上图可以看到,进程可能有以下几种状态。并且运行、等待和睡眠这三种状态之间是可以互相转换的。

  • 运行:该进程此刻正在执行。
  • 等待:进程能够运行,但没有得到许可,因为 CPU 分配给另一个进程。调度器可以在下一次任务切换时选择该进程。
  • 睡眠:进程正在睡眠无法运行,因为它在等待一个外部事件。调度器无法在下一次任务切换时选择该进程。
  • 终止:进程终止。

知道了任务调度相关的原理后,怎样根据这些原理性知识来优化应用场景的速度呢?实际上,我们对进程的优先级做不了太大的改变,即使改变了也产生不了太大的作用,但是前面提到了线程实际是轻量级的进程,同样遵循上面的调度原理和规则,所以我们真正落地的场景在线程的优化上。基于任务调度的原理,我们可以衍生出这 2 类的优化思路:

  1. 提高线程的优先级:对于关键的线程,比如主线程,我们可以提高它的优先级,来帮助我们提升速度。除了直接提高线程的优先级,我们还可以将关键线程绑定 CPU 的大核这一种特殊的方式来提高该线程的执行效率。

  2. 减少线程创建或者状态切换的耗时:这一点可以通过在线程池中设置合理的常驻线程,线程保活时间等参数来减少线程频繁创建或者状态切换的耗时。因为线程池非常重要,我们后面会专门用一节课来详细讲解。

小结

在这一节中,我们详细介绍了影响程序速度的三个本质因素,并基于这三个因素,介绍了许多衍生而来优化思路,这其实就是一种自下而上的性能优化思路,也就是从底层原理出发去寻找方案,这样我们在进行优化时,才能更加全面和体系。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/9141.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

uniapp 中 的progress加载进度条 的使用,在 页面显示数据加载的进度条,使用户的使用体验效果更好

学习目标: 学习目标如下: 例如: uniapp 中 的progress加载进度条 的使用,在 页面显示数据加载的进度条,使用户的使用体验效果更好 学习内容: 学习内容如下所示: 相关属性的说明 进度条的显…

[ELK安装篇]:基于Docker虚拟容器化(主要LogStash)

文章目录 一:前置准备-(参考之前博客):1.1:准备Elasticsearch和Kibana环境:1.1.1:地址:https://blog.csdn.net/Abraxs/article/details/128517777 二:Docker安装LogStash(数据收集引擎&#xff…

【C#】医学实验室云LIS检验信息系统源码 采用B/S架构

基于B/S架构的医学实验室云LIS检验信息系统,整个系统的运行基于WEB层面,只需要在对应的工作台安装一个浏览器软件有外网即可访问,技术架构:Asp.NET CORE 3.1 MVC SQLserver Redis等。 一、系统概况 本系统是将各种生化、免疫、…

【MySQL】内置函数

目录 一、日期函数 1、获得年月日 2、获得时分秒 3、获得时间戳 4、在日期的基础上加日期 5、在日期的基础上减去时间 6、计算两个日期之间相差多少天 7、案例 二、字符串函数 1、获取emp表的ename列的字符集 2、要求显示exam_result表中的信息 3、求学生表中学生姓…

【Git】git仓库完整迁移

代码仓库,在公司有两个团队在做,并且gitlab所在环境不互通。有一个团队做的时间久一点,另一个团队想要用并做一些定制。就需要将代码转移到另一个gitlab管理。 参考:【Git】git仓库完整迁移(代码,分支&…

全志F1C200S嵌入式驱动开发(lcd屏幕驱动)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 lcd RGB屏幕作为比较经济、实用的显示工具,在实际场景中使用较多。它的信号来说,一般也比较简单,除了常规的数据信号,剩下来就是行同步、场同步、数据使能和时钟信号了。数据信…

el-table 表格头部合并

<el-table v-loading"listLoading" :key"tableKey" :data"list" stripe border fit highlight-current-rowstyle"width: 100%;" size"mini"><el-table-column label"第一行" align"center">…

免费的音频转换器带你突破音频格式束缚

曾经有一个名叫吴欢的音乐爱好者&#xff0c;他热爱收集各种音频文件&#xff0c;从经典的老歌到新潮的流行曲&#xff0c;样样都不放过。然而&#xff0c;他遇到了一个令人头疼的问题&#xff1a;因为音频格式的不同&#xff0c;他无法将一些珍贵的音乐记录转化为文字形式。他…

面试题 汇总

一、 七层模型和五层模型以及对应的作用 二、 TCP和UDP的区别 UDPTCP是否连接无连接面向连接是否可靠不可靠传输,不使用流量控制和拥塞控制可靠传输,使用流量控制和拥塞控制连接对象个数支持一对一,一对多,多对一和多对多交互通信只能是一对一通信传输方式面向报文面向字节…

01 Excel常用高频快捷键汇总

目录 一、简介二、快捷键介绍2.1 常用基本快捷键1 复制&#xff1a;CtrlC2 粘贴&#xff1a;CtrlV3 剪切&#xff1a;CtrlX4 撤销&#xff1a;CtrlZ5 全选&#xff1a;CtrlA 2.2 常用高级快捷键1 单元格内强制换行&#xff1a;AltEnter2 批量输入相同的内容&#xff1a;CtrlEnt…

vue3 - element-plus 上传各种 word pdf 文件、图片视频并上传到服务器功能效果,示例代码开箱即用。

效果图 在 vue3 项目中,使用 element plus 组件库的 el-upload 上传组件,进行文件、图片图像的上传功能示例。 完整代码 可直接复制,再改个接口地址。 在这里上传图片和文件是分成

嵌入式和 Java 走哪条路?

JAVA和嵌入式各有千秋&#xff0c;看个人取舍。 想挣钱挣得快一点&#xff0c;挣得多一点&#xff0c;那就选Java&#xff0c;但有中年危机。 想细水长流一点的&#xff0c;选嵌入式&#xff0c;挣钱挣得慢一点&#xff0c;也稳一点&#xff0c;挣得久一点&#xff0c;中年危…

IO进、线程——标准文件IO和时间函数

1.文件IO 最直观的系统调用 1.1打开文件 int open(const char *pathname, int flags, mode_t mode);功能&#xff1a;打开/创建后打开一个文件 返回值&#xff1a;成功返回文件描述符&#xff0c;失败-10 —— 标准输入 1 —— 标准输出 2 —— 标准出错参数说明&#xf…

linux操作历史history定制

history记录 Linux中历史操作记录history是一个很有用的功能&#xff0c;有时忘记了&#xff0c;翻翻以前的命令&#xff0c;十分方便。 # 展示所有历史记录 history # 筛选历史记录 history | grep nginx # 清除全部记录 -c history -c # 指定删除某一行,15是行号 history -…

ACwing 1081. 度的数量

文章目录 题意思路代码 题意 给你一段区间[x, y]求其中满足一个数恰好等于K个互不相等的B的整数次幂之和的数的个数。 例如&#xff1a;x 15, y 20, k 2, b 2&#xff0c;那么对于这个区间有且仅有三个数满足题意&#xff1a; 17 2 4 2 0 10001 17 2^42^0 10001 1724…

linux | vscode | makefile | c++编译和调试

简单介绍环境&#xff1a; vscode 、centos、 gcc、g、makefile 简单来说就是&#xff0c;写好项目然后再自己写makefile脚本实现编译。所以看这篇博客的用户需要了解gcc编译的一些常用命令以及makefile语法。在网上看了很多教程&#xff0c;以及官网也看了很多次&#xff0c;最…

前端开发实习总结参考范文

▼前端开发实习总结篇四 读了三年的大学&#xff0c;然而大多数人对本专业的认识还是不那么透彻&#xff0c;学的东西真正能够学以致用的东西很少&#xff0c;大家都抱怨没有实践的机会&#xff0c;在很多同学心里面对于本专业还是很茫然。直到即将毕业的时候才知道我们以前学…

uni-app中的uni.requireNativePlugin()

这个方法是用来引入原生插件的方法&#xff0c;自 HBuilderX 1.4 版本起&#xff0c;uni-app 支持引入原生插件&#xff0c;使用方式如下&#xff1a; const PluginName uni.requireNativePlugin(PluginName); // PluginName 为原生插件名称 引入插件的类型有三种&#xff1…

东南大学齿轮箱故障诊断(Python代码,CNN结合LSTM模型)

运行代码要求&#xff1a; 代码运行环境要求&#xff1a;Keras版本>2.4.0&#xff0c;python版本>3.6.0 1.东南大学采集数据平台&#xff1a; 数据 该数据集包含2个子数据集&#xff0c;包括轴承数据和齿轮数据&#xff0c;这两个子数据集都是在传动系动力学模拟器&am…

【计算机网络】计算机网络基础知识总结(秋招篇)

文章目录 前言计算机网络笔记TCP和UDP分别是什么 有什么区别基于TCP UDP这两个协议的上层协议有哪些&#xff1f;TCP和UDP分别在哪些领域被用的多&#xff1f;TCP实现可靠性传输用了哪些技术&#xff1f;&#xff08;TCP如何实现可靠性传输&#xff09;讲一下超时重传和超时定时…