Optimisation de la vitesse : re-comprendre l'optimisation de la vitesse

L'optimisation de la vitesse des applications est la plus fréquemment utilisée et l'une des optimisations d'applications les plus importantes.Elle comprend l'optimisation de la vitesse de démarrage, l'optimisation de la vitesse d'ouverture des pages, l'optimisation de la vitesse d'exécution des fonctions ou de l'entreprise, etc., ce qui peut directement améliorer l'expérience utilisateur de l'application. Par conséquent, tant que vous êtes un développeur Android, vous devez avoir plus ou moins d'expérience dans l'optimisation liée à la vitesse. Mais lorsque la plupart des gens parlent d'optimisation de la vitesse, ils ne peuvent penser qu'à quelques points d'optimisation fragmentaires, tels que l'utilisation du multithreading, du préchargement, etc. Ce n'est certainement pas suffisant pour améliorer la vitesse, si l'on veut faire mieux, autant réfléchir aux questions suivantes :

  • Notre plan d'optimisation est-il complet et systématique ?

  • Pourquoi notre solution peut-elle améliorer la vitesse ?

  • Quelle est l'efficacité de notre plan?

Pour bien répondre à ces questions, nous devons comprendre les principes sous-jacents et l'essence qui affectent et déterminent la vitesse d'application. Du point de vue inférieur, le processeur, le cache et la planification des tâches sont les facteurs les plus essentiels qui déterminent la vitesse de l'application. Le processeur et le cache appartiennent tous deux à la couche matérielle, et le mécanisme de planification des tâches appartient à la couche du système d'exploitation.

Dans cette classe, nous allons approfondir le matériel et le système d'exploitation pour comprendre comment les trois facteurs ci-dessus déterminent la vitesse de l'application, comprendre à nouveau l'optimisation de la vitesse de l'application et établir un système cognitif et une méthode d'optimisation de la vitesse de bas en haut.

Comment optimiser la vitesse au niveau du CPU ?

Nous savons que tous les programmes seront éventuellement compilés en instructions de code machine, puis remis au CPU pour exécution.Le CPU exécute les instructions de code machine du programme une par une sous la forme d'un pipeline. Lorsque l'on veut augmenter la vitesse de certains scénarios (comme le démarrage, l'ouverture de page, le glissement, etc.), il s'agit essentiellement de réduire le temps d'exécution des instructions par le CPU dans ces scénarios, que l'on appelle temps CPU en abrégé. . Pour réduire le temps CPU, il faut connaître la formule de calcul du temps CPU consommé par le programme : temps CPU = nombre d'instructions du programme x temps de cycle d'horloge x nombre de cycles d'horloge moyen de chaque instruction . La signification de ces trois facteurs est expliquée ci-dessous.

  • Le nombre d'instructions du programme : Cet élément est bien compris, qui est le nombre d'instructions après que le programme est compilé en instructions de code machine.

  • Temps de cycle d'horloge : la CPU n'exécute qu'une seule exécution par cycle d'horloge, donc plus le temps de cycle d'horloge est court, plus la CPU s'exécute rapidement. Peut-être que vous n'êtes pas familier avec le temps de cycle d'horloge, mais sa réciproque est la fréquence de cycle d'horloge, vous devez en avoir entendu parler. Le temps de cycle d'horloge de 1 nanoseconde correspond à la fréquence de cycle d'horloge de 1 GHz. Lorsque les fabricants lancent de nouveaux téléphones mobiles ou que nous achetons de nouveaux téléphones mobiles, ils mentionnent plus ou moins la fréquence d'horloge du processeur, comme la fréquence d'horloge du Qualcomm. Processeur Snapdragon 888. Il est de 2,8 GHz, qui est également l'indicateur le plus important pour mesurer les performances du processeur .

  • La durée moyenne de chaque instruction : c'est la durée moyenne consommée par l'exécution de l'instruction, et le nombre de cycles machine nécessaires pour différentes instructions est également différent. Pour certaines instructions simples à un octet, dans le cycle d'extraction d'instructions, l'instruction sera décodée et exécutée immédiatement après avoir été extraite dans le registre d'instructions, et aucun autre cycle machine n'est requis. Pour certaines instructions plus complexes, telles que les instructions de transfert et les instructions de multiplication, deux cycles machine ou plus sont nécessaires.

Du point de vue du processeur, lorsque nous voulons augmenter la vitesse du programme, l'optimisation de l'un de ces trois facteurs peut atteindre l'objectif. Alors, quelles solutions générales peuvent être utilisées comme référence en fonction de ces trois facteurs ?

Réduire le nombre d'instructions dans le programme

Augmenter la vitesse en réduisant le nombre d'instructions dans le programme est la méthode la plus couramment utilisée et la plus optimisée.Par exemple, les solutions suivantes augmentent la vitesse en réduisant le nombre d'instructions.

  1. Utilisation du multicœur du téléphone mobile : Lorsque l'on passe les instructions de programme de la scène à accélérer à plusieurs CPU pour une exécution simultanée, pour un seul CPU, le nombre d'instructions à exécuter est réduit, et le CPU le temps est naturellement réduit, c'est-à-dire pensé simultanément. Mais il convient de noter que la concurrence ne peut être obtenue que sous multi-cœur.S'il n'y a qu'un seul processeur, même si nous découpons les instructions de la scène en plusieurs copies, pour ce processeur, le nombre d'instructions de programme ne diminuera toujours pas. Comment tirer parti du multicœur de la machine ? Utilisez simplement le multithreading. Si notre téléphone mobile a 4 cœurs, nous pouvons exécuter 4 threads simultanément.

  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 级。下图展示的是三层高速缓存的存储结构。

img

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

img

不同层之间的读写速度差距是很大的,所以为了能提高场景的速度,我们需要将和核心场景相关的资源(代码、数据等)尽量存储在靠上层的存储器中。 基于这一原理,便能衍生出了非常多的优化方案,比如常用的加载图片的框架 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 中,除了部分核心进程,其他大部分都是普通进程。

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

img

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

  • 运行:该进程此刻正在执行。

  • 等待:进程能够运行,但没有得到许可,因为 CPU 分配给另一个进程。调度器可以在下一次任务切换时选择该进程。

  • 睡眠:进程正在睡眠无法运行,因为它在等待一个外部事件。调度器无法在下一次任务切换时选择该进程。

  • 终止:进程终止。

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

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

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

小结

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

希望你通过这一节的学习,能对速度优化建立起一个体系的认知。当然,你可能会觉得我们这一节介绍的优化思路太过简洁,不必担心,在后面的章节中,我们会基于 CPU、缓存和任务调度这三个维度,挑选出一些优化效果较好的方案,进行更加深入的详细讲解。

Guess you like

Origin juejin.im/post/7235279096312856637