再谈性能优化,一次项目优化经历分享

1. 前言

很高兴见到你。

关于性能优化,前面我已经写过一篇关于性能优化相关的文章,可以参考下,传送门:聊一聊性能优化,主要是分享一些简单的性能优化点,以及介绍什么是性能优化。

性能优化最本质的作用,无非就是两个方面:时间和空间。cpu、内存资源是有限的,如何在有限的资源,做更多的事情,就是性能优化的目标。性能差的程序带来的最大的问题,就是卡顿、闪退、发热、耗时等。但很多时候,在实际开发中,为了交付需求,往往会忽略了性能需求。例如在我们部分的外包同学,代码架构、性能往往都存在问题,导致后期需要专门腾出时间来做性能优化和重构。

这篇文章,主要基于我在项目做的一次比较完整的性能优化,分享一些关于性能优化的经验。文章会从我项目的问题出发,一步步来介绍我做性能优化的思路。希望对你有帮助。

那么,我们开始吧。

2. 项目介绍

我们项目的核心功能,是在端侧进行图像识别,并使用识别的结果完成后续的业务功能。在前台界面上会有一个进度条,等待进度条走完那么用户就可以看到结果。

在实际的体验中,我们发现一个问题:耗时实在是太久了,大部分用户等不及就离开了。从开始到结束看到结果,需要经历好几分钟的时间,迫切需要对耗时进行优化,产品预期希望一分钟内看到结果。

其次,我们也发现了很多的发热、卡顿现象。特别是在一些性能比较一般的机器中,尤为明显,整体的体验是比较差的。

3. 流程梳理

在进行性能优化之前,我们需要先理清,这个功能的整体流程是什么。这样才能知道哪些流程存在问题、需要优化。

核心流程可以总结为:读取图片到内存 -> 端侧算法内容识别 -> 业务逻辑。但完整的工程链路是比较长的,详细的流程参考下图:

流程很长哈,基本上分为三个阶段,简单介绍一下:

  • 在进行内容识别之前,有一个比较长的准备阶段,需要做一些预处理,如读取图片exif、下载算法模型等
  • 算法初始化结束后,循环进行内容识别,不断地对图片进行算法处理,并最后输出识别结果
  • 业务侧对输出的图像识别结果上传到服务端进行处理,最后回到本地进行展示

这是一个隐藏了业务细节和简化后的版本,但对于我们理解性能优化的问题也足够了。

在上述梳理流程的基础上,我们还需要对整体的耗时情况有一个概念,这样才能抓住重点。耗时情况如下:

到这里我们也就可以发现了,核心耗时主要有三个阶段,而且每个阶段的耗时都特别高:

  • 初始化阶段
  • 图像算法识别
  • 服务端数据处理

接下来我们就针对这些方面进行分析优化。

4. 耗时优化方案

多线程优化图像识别

首先针对算法分析阶段进行优化,他的流程如下:

  • 每一个媒体文件,都需要先经历读取素材bitmap,再进行算法识别的串行的流程
  • 读取素材bitmap等操作属于IO密集型,而算法图像识别属于CPU密集型,这两者并行化可以提高效率
  • 这种方式流程简单,但是整体的识别速度很慢,在高端机中cpu的利用程度低。单线程算法图片分析并没有利用好cpu资源,可以通过增加算法分析线程,来提高图像的识别速度。

因此我们需要做两件事:将读取图片操作和算法识别并行化;增加多线程来处理这两个任务。基于这个思路,我们使用生产-消费模型来设计整体的流程框架。

  • 整体模型参考 生产者-消费者 模型,将 读取图像 设置为生产者任务, 算法识别 设置为消费者任务
  • 通过增加线程,将生产任务和消费任务并行化,任务队列作为两端之间的缓冲。速度控制用于在消费者消费完一个任务后,中间的暂停间歇时间,可用于均衡cpu负载。
  • 模型的最大效率,取决于两端线程的上限,在充分利用cpu资源未耗尽的前提下,线程越多,效率越高;而反之,cpu资源耗尽,线程越多则效率越低。

基于这个模型,可以实现多线程任务处理、图像读取任务和算法图像识别并行化,达到我们的目的。但目前我们还有一些重要的问题需要解决:如何配置这个模型的参数,如队列长度、线程数量,来达到最大的效率?

算法识别对于手机的性能压力是比较大的,模型的瓶颈在于,如何尽可能多地跑算法分析。而图像读取是比较快的,且主要占用IO资源。因此,如何让消费端将性能打满是一个重要的目标,生产端的速度只要不拖后腿即可。

队列长度和生产者的参数如何配置? 这块的参数只需要满足消费者的使用即可,尽量不要让消费者暂停下来。经过实际运行,生产者的平均速度大约为消费者的一半,因此生产线程的数量在消费线程的2倍即可。 队列长度的设置目的是缓冲,同时为了避免生产者频繁暂停唤醒,长度设置为 生产者数量x2 + 2即可。生产者的数量也不宜过大,过大会导致生产线程频繁挂起和唤醒,也会影响性能。

在我们的项目中,经过这个优化,在高端机中,整体耗时可以降低50%以上,妥妥的优化利器。

初始化阶段优化

优化完算法识别,我们再看回来初始化阶段的流程:

初始化阶段做的事情很多,且都是耗时操作。但是仔细一看可以发现,有一些任务之间是没有依赖关系的,处理为并行操作可以减少很多的耗时。如下:

并行化后,由于读取素材的exif、请求POI等操作耗时明显较长,约等于节省掉下载算法模型和RPC请求的耗时。

那这就是初始化阶段优化的极限了吗?我们可以看到读取exif和POI请求耗时还是非常久的,特别是在于用户的素材特别多的情况下。我们真的需要在初始化阶段去将所有素材的exif和POI完全获取吗?答案是否定的,因为素材的这些信息只在算法识别的时候做为辅助信息进行处理。那么优化方案就浮出水面了:定义一个独立模块异步处理这两块任务,外部需要的时候读取即可。优化后的流程如下:

经过这个优化后,初始化阶段的耗时基本可以控制在 下载算法模型 + 算法初始化 这两块了,从103s 优化到 33s。

再一次提问,这就是极限了吗?对于这些任务,本身并不是高负载任务,可以将这些任务进行预处理,在进入该功能之前处理完成,那么可以进一步压缩耗时时间。但需要注意的是,下载算法模型需要占用带宽,需要和其他业务明确好,不能影响了其他的业务;又例如读取素材exif,这需要相册权限,也需要在合适的时机去完成。

这里还有一个重点需要提一下。我们可以看到这里面用了非常多的线程,初始化阶段、exif读取、算法识别、图像读取等等,如果真的都创建了线程会导致app的线程数量突增,可能并达不到理想的优化效果。

所以这里需要使用线程池来进行统一管理和优化,将每个地方的线程数量 修改为 最大并发量。例如原本我们可能需要增加3个线程来负责图像读取,那么这时就需要改为使用线程池并限制该任务的最大并发量为3。可能没办法达到最理想的优化效果,但是能避免线程数量爆炸带来的劣化。

其他方面优化

我们上述的优化,都是针对在客户端,也就是我们自己负责的模块。这轮优化下来之后可以看到耗时降低了不少,但是整体的耗时还是比较高的。如果需要再进一步优化,那么我们需要考虑从其他方向入手,例如服务端等。我们一个个来。

服务端的整体时间在64s,肯定是还存在优化空间的。在实际的项目中也是,经过和服务单的协调优化后,最终他们的耗时可以降低一半,在30s左右。

第二个地方是算法。算法有两个地方可以优化:算法模型大小、算法图像识别速度。这块在项目中经过优化后,模型大小减少70%,识别耗时降低50%。

到这里我们的总耗时已经优化到了:90s左右。距离产品的1min还是差了比较多。但基本上我们能做的都做了,那该怎么办呢?调整产品策略

我们可以看到,我们是将所有的图片分析结束后,再产出结果。但可以有一种方法是:只需要分析一部分,就先产出一个结果,给到用户反馈。再在后台不断分析剩下的图片,结束后再给予用户完整的结果。这就有点类似我们在去餐厅点餐,是一次性将菜品上齐,还是好了一个菜品就上一个,显然后者会有更好的用户体验。

经过产品策略调整后,整体的耗时下降到50s以内,满足产品预期需求。

这里的话就有一个很重要的思路:以产品需求为目标,视角不局限于自身模块。假如只针对客户端进行优化,那么怎么做都很难达到60s的耗时预期。当推动了算法、服务单、产品调整后,这个结果就能够很快达成,也更加合理。

代码细节优化

上面我们所讲的优化,都是基于业务流程来进行的优化。但其实也有相当一部分优化,在于代码的质量。例如重复加载bitmap、在主线程做耗时操作等等,需要我们去根据具体的代码细节进行优化。那这块怎么做呢?

第一是在写代码的时候,注重代码的性能表现。我们常说的算法题是一个很好的思路,通过思考代码的时间和空间属性,来提高代码的性能。也有一些书籍可以参考,例如 《Effective Java》等。

第二是通过一些工具来做性能检测。常见的问题就是内存泄露和UI卡顿。Android studio中有一个很好用的工具 Profiler,他可以检测内存分配的函数、在某个卡顿的帧做了什么事情。也可以打开strictMode,通过日志来看哪些耗时的操作在主线程运行。

业务流程的优化是很好通过后期的分析来提升性能,但是代码的质量往往很难,需要仔细阅读代码细节才能发现其中的问题。所以不断提升对性能的敏感度,并积累一些高性能代码的知识,才是重中之重。

5. 负载控制

那现在耗时目标已经达到,这优化就结束了吗?

当然不是。现在还有一个更重要的事情:机器负载。我们上述的优化,新建了很多的线程,整机的cpu、内存负载都上升不少。在机器性能比较差的机型上,就会导致比较严重的问题:卡顿、发热、闪退。其次,即使在性能比较好的机器,他也可能当前机器的负载本身就比较高,例如后台挂着个游戏等。因此我们需要根据机器的具体情况,控制好负载程度,避免这些问题的出现,提升功能的稳定性。

首先我们需要明确一个点:如何控制负载程度。

回顾我们前面的整体流程,可以发现最核心的负载因素就是图像算法识别这个过程。前面的读取exif、RPC请求等都是以IO为主,影响性能表现比较小。而决定图像算法识别负载的因素是:最大算法识别并发数、每次识别结束后的暂停时间

最大算法识别并发数 决定了同时最多有多少个算法实例在运行,也决定了队列缓存的数量,直接影响了CPU负载和内存负载。

每次识别结束后的暂停时间主要是用于均衡cpu负载,在一些性能比较差的机器中,开一个线程对于cpu负载可能就已经比较高了,那么为了可以运行此功能,可以每识别结束一个图片,休息500ms。这样一段时间内整体上cpu负载就可以得到降低了。

现在我们已经明确了如何控制机器的负载,那么该怎么去设置负载参数呢?为了解决这个问题,我们引入了两个方案:静态分级和动态降级

静态分级,指的是通过评价机器本身的硬件条件,来决策他们运行的最大负载。硬件条件包括 SOC、内存等。在我们的项目中,主要的评价对象就是机器的SOC。我们根据机器的SOC条件,划分出了4个档位:

  • 低端机:无法运行功能
  • 中端机:可以单线程运行,但需要增加休息时间
  • 高端机:可以单线程运行
  • 顶端机:可以多线程运行

具体划分的方式,是通过寻找对应soc的机器进行运行,测量他的负载表现来决策。

静态分级是比较粗粒度的,而且无法代表当前运行机器的实际情况。例如可能当前的手机正打着游戏、或者处在高温环境等。在发热之后,实际的运行效率会降低很多,手机的soc也会进行降频,此时如果强行开启多线程优化可能会适得其反,而且会导致手机发热严重。那么我们还需要根据用户机器的实际情况,进行动态降级。

动态降级的因素可以依据:电池温度、内存占用、CPU占用。电池温度是最能表现目前手机的状态的,温度过高的时候代表当前机器情况较差。内存占用过高也会导致运行效率下降,或者OOM,需要降低并发数来降低内促压力。CPU占用一般来说波动比较大,如果连续几次检测到都比较高,那很可能目前机器的负载就是很高。

下面举一个应用例子来说明:

  1. 启动时,根据用户剩余的内存和电池温度来决定是否开启多算法实例 a. 一个算法实例大概200mb,至少需要1GB剩余内存才能够开启多算法实例 b. 温度需要低于38度
  2. 运行时,每间隔10次图像运行,监控一次CPU状态和电池温度,来调整分析间隔 a. 每次调整100ms,最高1000ms,最低按照机型静态分级来配置 b. 按照38-42度,cpu负载50-100平均划分。例如起始间隔是0,温度40度,但是cpu负载只有60%,那么最高就只能配置500ms。
  3. 根据场景来配置:前台全量运行,离开前台后需要增加休息间隔来减少压力。

当然这些数据都是不严谨的,实际操作的时候,需要实际真机去运行、以及产品设计等来共同决策。

这个思路可以用到很多的高负载功能下,例如直播、视频编辑等等,核心思路就是 负载调整+静态分级+动态降级

7. 最后

好了,到这里关于这个项目的性能优化就讲完了,我们再来回顾一下整体的性能优化流程:

  1. 明确性能优化的目标,例如是降低耗时、降低内存占用,目标的水位是多少。
  2. 梳理整体流程和每个细节流程的耗时,确定需要优化的环节。
  3. 从全链路,多个角度来进行优化:自己负责的模块、其他模块链路、产品设计。
  4. 优化的方式可以有:多线程并行、预处理、流程简化、代码细节优化等。
  5. 需要注意整体功能的负载,通过负载调节 + 静态分级 + 动态降级来进行调整。

我还是之前的观点,性能优化不是高深莫测的东西,而是在我们开发的方方面面。这篇文章并没有深入到代码细节,而是主要着重于业务流程、机器负载等方面来聊,具体到执行还有很多的事要做。

希望文章对你有帮助。

相关推荐
拾忆,想起3 小时前
Redisson 分布式锁的实现原理
java·开发语言·分布式·后端·性能优化·wpf
189228048614 小时前
NW622NW623美光固态闪存NW624NW635
大数据·网络·数据库·人工智能·microsoft·性能优化
雮尘4 小时前
Android性能优化之枚举替代
android
2501_915909066 小时前
苹果上架App软件全流程指南:iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传与审核技巧详解
android·ios·小程序·https·uni-app·iphone·webview
2501_915921436 小时前
iOS 文件管理与能耗调试结合实战 如何查看缓存文件、优化电池消耗、分析App使用记录(uni-app开发与性能优化必备指南)
android·ios·缓存·小程序·uni-app·iphone·webview
文人sec6 小时前
性能测试-jmeter10-分布式测试
分布式·jmeter·性能优化·模块测试
波波烤鸭6 小时前
Spring Boot 原理与性能优化实战
spring boot·后端·性能优化
2501_915918416 小时前
App 苹果 上架全流程解析 iOS 应用发布步骤、App Store 上架流程
android·ios·小程序·https·uni-app·iphone·webview
2501_916007477 小时前
苹果上架全流程详解,iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传与审核要点完整指南
android·ios·小程序·https·uni-app·iphone·webview