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

news2024/11/29 5:59:26

作者: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.coloradmin.cn/o/792822.html

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

相关文章

小程序----配置原生内置编译插件支持sass

修改project.config.json配置文件 在 project.config.json 文件中,修改setting 下的 useCompilerPlugins 字段为 ["sass"], 即可开启工具内置的 sass 编译插件。 目前支持三个编译插件:typescript、less、sass 修改之后可以将原.w…

Qt Core学习日记——第三天QMetaEnum(上)

QMetaEnum用来代表枚举信息,内部也是访问moc文件。从moc文件中得到对应值 需要在头文件中声明 Q_ENUM,如下红框部分 moc中qt_meta_stringdata_XTest变为: qt_meta_data_XTest变为 static const uint qt_meta_data_XTest[] { // content: 8, // revision 0, // …

继承中的访问级别

值得思考的问题 子类是否可以直接访问父类的私有成员? 思考过程 继承中的访问级别 面向对象中的访问级别不止是 public 和 private 可以定义 protected 访问级别 关键字 protected 的意义 修饰的成员不能被外界直接访问修饰的成员可以被子类直接访问 思考 为什…

【数据库 - 用户权限管理】(简略)

目录 一、概述 二、用户权限类型 1.ALL PRIVILEGES 2.CREATE 3.DROP 4.SELECT 5.INSERT 6.UPDATE 7.DELETE 8.INDEX 9.ALTER 10.CREATE VIEW和CREATE ROUTINE 11.SHUTDOWN 12GRANT OPTION 三、语句格式 1.用户赋权 2.权限删除 3.用户删除 一、概述 数据库用…

Emacs之27.0以上共享鼠标中键复制内容(一百一十七一)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生…

264. 丑数 II

题目描述&#xff1a; 主要思路&#xff1a; 利用动态规划的思想&#xff0c;记录2 3 5分别乘到了哪里&#xff0c;然后取最小作为新的数字。 class Solution { public:int nthUglyNumber(int n) {int dp[n1];dp[1]1;int p21,p31,p51;for(int i2;i<n;i){int num2 dp[p2]*…

【概率预测】对风力发电进行短期概率预测的分析研究(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f308;4 Matlab代码、数据、详细文章 &#x1f4a5;1 概述 概率预测是一种通过概率统计方法对未来事件进行预测的技术。在风力发电的短期预测中&#xff0c;概率预测可以用来对未来风速和风…

自动生成python程序调用关系逻辑图

前言 你是否因为看一个程序或者运行一个框架&#xff0c;不知道他的运行流程&#xff1f; 自己想写一个运行流程却觉得麻烦无从下手&#xff1f; graphvizpycallgraph帮你绘制让领导看了都拍桌子称赞你的python程序逻辑调用关系图&#xff01; 先来看一下我这段时间在写的一…

60寸透明屏的透明度怎么样?

60寸透明屏是一种新型的显示屏技术&#xff0c;它具有透明度高、色彩鲜艳、清晰度高等特点&#xff0c;可以广泛应用于商业展示、户外广告、智能家居等领域。 首先&#xff0c;60寸透明屏的透明度高。 透明屏采用了特殊的材料和技术&#xff0c;使得屏幕在显示内容的同时&…

2023-07-25 LeetCode每日一题(将数组和减半的最少操作次数)

2023-07-25每日一题 一、题目编号 2208. 将数组和减半的最少操作次数二、题目链接 点击跳转到题目位置 三、题目描述 给你一个正整数数组 nums 。每一次操作中&#xff0c;你可以从 nums 中选择 任意 一个数并将它减小到 恰好 一半。&#xff08;注意&#xff0c;在后续操…

uni-app云打包(android)(自有证书、云端证书、公共测试证书)

一、进入云打包入口 发行->原生App-云打包 二、证书选择 1、使用自有证书 ①进入香蕉云编&#xff08;这里采用的证书从香蕉云编进行生成&#xff09; 香蕉云编-app打包上架工具类平台 ②进入页面选择“生成签名证书”->"立即创建证书" ③选择“安卓证书生…

一些python的高级方法(闭包、装饰器、多线程详解)

目录 闭包 装饰器 普通用法 多层装饰器 设计模式 单例模式 工厂模式 多线程 基础使用 得到当前的线程 守护线程 线程阻塞join方法 线程锁 Lock 递归锁对象RLock 闭包 例如要实现一个存钱的功能&#xff0c;可以这么做 account_amount 0def atm(num,deposit Tr…

linux学成之路(基础篇(二十三)MySQL服务(中)

目录 MySQL服务之SQL语句 一、SQL语句类型 一、 DDL语句 二、DML语句 三、DCL语句 四、DQL 语句 二、 数据库操作 一、查看 二、创建 三、进入 四、删除数据库 五、更改数据库名称 六、更改字符集 三、数据表管理 一、数据类型 一、数值类型 TINYINT SMALLINT…

组件间通信案例练习

1.实现父传子 App.vue <template><div class"app"><tab-control :titles["衣服","鞋子","裤子"]></tab-control><tab-control :titles["流行","最新","优选","数码&q…

网络安全 Day08-Linux文件属性知识

Linux 文件属性知识 1. 查看文件属性2. 文件属性知识 1. 查看文件属性 语法&#xff1a;ls -lhi&#xff08;l-长格式 h-人类可读&#xff09;结果[rootlocalhost ~] ls -lhi total 11M 202312369 drwxr-xr-x. 31 root root 4.0K Jul 30 2023 1 134317954 -rw-------. 1 roo…

SpringBoot实战(二十二)集成 Sleuth、Zipkin

目录 一、简介1.Sleuth2.Zipkin 二、搭建 zipkin-server1.jar包启动2.docker启动3.自己搭建**Maven依赖**添加启动类注解 4.页面截图 三、搭建 sleuth-zipkin1.Maven 依赖2.yaml配置3.代码实现DemoController.javaDemoFeignClient.java 4.测试 一、简介 1.Sleuth 官方文档&am…

安全技术-大数据平台安全防护技术

一、大数据基本概念及背景 1.1大数据发展的背景-数据爆炸 伴随着互联⽹、物联⽹、电⼦商务、社交媒体、现代物流、⽹络⾦融等⾏业的发展&#xff0c;全球数据总量正呈⼏何级数增长&#xff0c;过去⼏年时间产⽣的数据总量超过了⼈类历史上的数据总和&#xff0c;预计2020年全…

json的序列化与反序列化

目录 json的下载 json的序列化 json的反序列化 备注json开源项目github地址&#xff1a;https://github.com/nlohmann/json 备注开发环境&#xff1a;vscode通过ssh连接虚拟机中的ubuntu&#xff0c;ubuntu-20.04.3-desktop-amd64.iso json的下载 git clone https://githu…

iptables的备份和还原

iptables的备份和还原 1、写在命令行当中的都是临时设置 2、把规则配置写在服务的文件当中&#xff0c;形成永久有效 备份&#xff1a;把iptables里面所有的配置都保存在/opt/ky30.bak中 iptables-save > /opt/ky30.bak 例&#xff1a; 默认配置文件在/etc/sysconfig/ip…

C++ const与指针

const与指针 1.const在C语言和C中的区别 &#xff08;1&#xff09;在C语言中 #include<stdio.h> int main() {const int n 10; int arr[n] { 1,2 }; //errorreturn 0; }在C语言中&#xff0c;const修饰的变量是只读变量&#xff08;常变量&#xff09;&#xff0c…