终端卡顿优化的全记录

简介: 目前手机SOC的性能越来越少,很多程序员在终端程序的开发过程中也不太注意性能方面的优化,尤其是不注意对齐和分支优化,但是这两种问题一旦出现所引发的问题,是非常非常隐蔽难查的,不过好在项目中用到了移动端的性能排查神器友盟U-APM工具的支持下,最终几个问题得到了圆满解决。

我们先来看对齐的问题,对齐在没有并发竞争的情况下不会有什么问题,编译器一般都会帮助程序员按照CPU字长进行对齐,但这在终端多线程同时工作的情况下可能会隐藏着巨大的性能问题,在多线程并发的情况下,即使没有共享变量,也可能会造成伪共享,由于具体的代码涉密,因此我们来看以下抽象后的代码。

public class Main {

        public static void main(String[] args) {

                 final MyData data = new MyData();

new Thread(new Runnable() {

public void run() {

data.add(0);

}

}).start();

new Thread(new Runnable() {

public void run() {

data.add(0);

}

}).start();

try{

Thread.sleep(100);

} catch (InterruptedException e){

e.printStackTrace();

}

long[][] arr=data.Getitem();

System.out.println("arr0 is "+arr[0]+"arr1 is"+arr[1]);

        }

}

class MyData {

private long[] arr={0,0};

public long[] Getitem(){

return arr;

}

public void add(int j){

for (;true;){

arr[j]++;

}

}

}

在这段代码中,两个子线程执行类似任务,分别操作arr数组当中的两个成员,由于两个子线程的操作对象分别是arr[0]和arr[1]并不存在交叉的问题,因此当时判断判断不会造成并发竞争问题,也没有加synchronized关键字

但是这段程序却经常莫名的卡顿,后来经过多方的查找,并最终通过友盟的卡顿分析功能我们最终定位到了上述代码段,发现这是一个由于没有按照缓存行进行对齐而产生的问题,这里先将修改完成后的伪代码向大家说明一下:

public class Main {

        public static void main(String[] args) {

                 final MyData data = new MyData();

new Thread(new Runnable() {

public void run() {

data.add(0);

}

}).start();

new Thread(new Runnable() {

public void run() {

data.add(0);

}

}).start();

try{

Thread.sleep(10);

} catch (InterruptedException e){

e.printStackTrace();

}

long[][] arr=data.Getitem();

System.out.println("arr0 is "+arr[0][0]+"arr1 is"+arr[1][0]);

        }

}

class MyData {

private long[][] arr={{0,0,0,0,0,0,0,0,0},{0,0}};

public long[][] Getitem(){

return arr;

}

public void add(int j){

for (;true;){

arr[j][0]++;

}

}

}

可以看到整体程序没有作何变化,只是将原来的数组变成了二维数组,其中除了第一个数组中除arr[0][0]元素外,其余arr[0][1]-a[0][8]元素除完全不起作何与程序运行有关的作用,但就这么一个小小的改动,却带来了性能有了接近20%的大幅提升,如果并发更多的话提升幅度还会更加明显。

缓存行对齐排查分析过程

首先我们把之前代码的多线程改为单线程串行执行,结果发现效率与原始的代码一并没有差很多,这就让我基本确定了这是一个由伪共享引发的问题,但是我初始代码中并没有变量共享的问题,所以这基本可以判断是由于对齐惹的祸。

现代的CPU一般都不是按位进行内存访问,而是按照字长来访问内存,当CPU从内存或者磁盘中将读变量载入到寄存器时,每次操作的最小单位一般是取决于CPU的字长。比如8位字是1字节,那么至少由内存载入1字节也就是8位长的数据,再比如32位CPU每次就至少载入4字节数据, 64位系统8字节以此类推。那么以8位机为例咱们来看一下这个问题。假如变量1是个bool类型的变量,它占用1位空间,而变量2为byte类型占用8位空间,假如程序目前要访问变量2那么,第一次读取CPU会从开始的0x00位置读取8位,也就是将bool型的变量1与byte型变量2的高7位全部读入内存,但是byte变量的最低位却没有被读进来,还需要第二次的读取才能把完整的变量2读入。

也就是说变量的存储应该按照CPU的字长进行对齐,当访问的变量长度不足CPU字长的整数倍时,需要对变量的长度进行补齐。这样才能提升CPU与内存间的访问效率,避免额外的内存读取操作。但在对齐方面绝大多数编译器都做得很好,在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间边界。也可以通过pragma pack(n)调用来改变缺省的对界条件指令,调用后C编译器将按照pack(n)中指定的n来进行n个字节的对齐,这其实也对应着汇编语言中的.align。那么为什么还会有伪共享的对齐问题呢?

现代CPU中除了按字长对齐还需要按照缓存行对齐才能避免并发环境的竞争,目前主流ARM核移动SOC的缓存行大小是64byte,因为每个CPU都配备了自己独享的一级高速缓存,一级高速缓存基本是寄存器的速度,每次内存访问CPU除了将要访问的内存地址读取之外,还会将前后处于64byte的数据一同读取到高速缓存中,而如果两个变量被放在了同一个缓存行,那么即使不同CPU核心在分别操作这两个独立变量,而在实际场景中CPU核心实际也是在操作同一缓存行,这也是造成这个性能问题的原因。

Switch的坑

但是处理了这个对齐的问题之后,我们的程序虽然在绝大多数情况下的性能都不错,但是还是会有卡顿的情况,结果发现这是一个由于Switch分支引发的问题。

switch是一种我们在java、c等语言编程时经常用到的分支处理结构,主要的作用就是判断变量的取值并将程序代码送入不同的分支,这种设计在当时的环境下非常的精妙,但是在当前最新的移动SOC环境下运行,却会带来很多意想不到的坑。

出于涉与之前密的原因一样,真实的代码不能公开,我们先来看以下这段代码:

public class Main {

        public static void main(String[] args) {

                 long now=System.currentTimeMillis();

int max=100,min=0;

long a=0;

long b=0;

long c=0;

for(int j=0;j<10000000;j++){

int ran=(int)(Math.random()*(max-min)+min);

switch(ran){

case 0:

a++;

break;

case 1:

a++;

break;

default:

c++;

}

}

long diff=System.currentTimeMillis()-now;

System.out.println("a is "+a+"b is "+b+"c is "+c);

        }

}

其中随机数其实是一个rpc远程调用的返回,但是这段代码总是莫名其妙的卡顿,为了复现这个卡顿,定位到这个代码段也是通过友盟U-APM的卡顿分析找到的,想复现这个卡顿只需要我们再稍微把max范围由调整为5。

public class Main {

        public static void main(String[] args) {

                 long now=System.currentTimeMillis();

int max=5,min=0;

long a=0;

long b=0;

long c=0;

for(int j=0;j<10000000;j++){

int ran=(int)(Math.random()*(max-min)+min);

switch(ran){

case 0:

a++;

break;

case 1:

a++;

break;

default:

c++;

}

}

long diff=System.currentTimeMillis()-now;

System.out.println("a is "+a+"b is "+b+"c is "+c);

        }

}

那么运行时间就会有30%的下降,不过从我们分析的情况来看,代码一平均每个随机数有97%的概念要行2次判断才能跳转到最终的分支,总体的判断语句执行期望为2*0.97+1*0.03约等于2,而代码二有30%的概念只需要1次判断就可以跳转到最终分支,总体的判断执行期望也就是0.3*1+0.6*2=1.5,但是代码二却反比代码一还慢30%。也就是说在代码逻辑完全没变只是返回值范围的概率密度做一下调整,就会使程序的运行效率大大下降,要解释这个问题要从指令流水线说起。

指令流水线原理

我们知道CPU的每个动作都需要用晶体震荡而触发,以加法ADD指令为例,想完成这个执行指令需要取指、译码、取操作数、执行以及取操作结果等若干步骤,而每个步骤都需要一次晶体震荡才能推进,因此在流水线技术出现之前执行一条指令至少需要5到6次晶体震荡周期才能完成

为了缩短指令执行的晶体震荡周期,芯片设计人员参考了工厂流水线机制的提出了指令流水线的想法,由于取指、译码这些模块其实在芯片内部都是独立的,完成可以在同一时刻并发执行,那么只要将多条指令的不同步骤放在同一时刻执行,比如指令1取指,指令2译码,指令3取操作数等等,就可以大幅提高CPU执行效率:

以上图流水线为例 ,在T5时刻之前指令流水线以每周期一条的速度不断建立,在T5时代以后每个震荡周期,都可以有一条指令取结果,平均每条指令就只需要一个震荡周期就可以完成。这种流水线设计也就大幅提升了CPU的运算速度。

但是CPU流水线高度依赖指指令预测技术,假如在流水线上指令5本是不该执行的,但却在T6时刻已经拿到指令1的结果时才发现这个预测失败,那么指令5在流水线上将会化为无效的气泡,如果指令6到8全部和指令5有强关联而一并失效的话,那么整个流水线都需要重新建立。

所以可以看出例子当中的这个效率差完全是CPU指令预测造成的,也就是说CPU自带的机制就是会对于执行概比较高的分支给出更多的预测倾斜。

处理建议-用哈希表替代switch

我们上文也介绍过哈希表也就是字典,可以快速将键值key转化为值value,从某种程度上讲可以替换switch的作用,按照第一段代码的逻辑,用哈希表重写的方案如下:

import java.util.HashMap;

public class Main {

        public static void main(String[] args) {

                 long now=System.currentTimeMillis();

int max=6,min=0;

HashMap<Integer,Integer> hMap = new HashMap<Integer,Integer>();

hMap.put(0,0);

hMap.put(1,0);

hMap.put(2,0);

hMap.put(3,0);

hMap.put(4,0);

hMap.put(5,0);

for(int j=0;j<10000000;j++){

int ran=(int)(Math.random()*(max-min)+min);

int value = hMap.get(ran)+1;

hMap.replace(ran,value);

}

long diff=System.currentTimeMillis()-now;

System.out.println(hMap);

System.out.println("time is "+ diff);

        }

}

上述这段用哈希表的代码虽然不如代码一速度快,但是总体非常稳定,即使出现代码二的情况也比较平稳。

经验总结

一、有并发的终端编程一定要注意按照缓存行(64byte)对齐,不按照缓存行对齐的代码就是每增加一个线程性能会损失20%。

二、重点关注switch、if-else分支的问题,一旦条件分支的取值条件有所变化,那么应该首选用哈希表结构,对于条件分支进行优化。

三、选择一款好用的性能监测工具,如:友盟U-APM,不仅免费且捕获类型较为全面,推荐大家使用。

原文链接
本文为阿里云原创内容,未经允许不得转载。 

 

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

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

相关文章

brew安装指定版本mysql,Mac 系统为 Valet 开发环境安装指定版本 MySQL

Mac 系统为 Valet 开发环境安装指定版本 MySQL由 学院君 创建于1年前, 最后更新于 5个月前版本号 #31547 views1 likes0 collects在 Mac 系统下使用 Valet 作为 Laravel 本地开发环境的话&#xff0c;需要自行安装 MySQL 数据库&#xff0c;我们通过 Homebrew 来安装。如果之前…

系统架构面临的三大挑战,看 Kubernetes 监控如何解决?

简介&#xff1a; 随着 Kubernetes 的不断实践落地&#xff0c;我们经常会遇到负载均衡、集群调度、水平扩展等问题。归根到底&#xff0c;这些问题背后都暴露出流量分布不均的问题。那么&#xff0c;我们该如何发现资源使用&#xff0c;解决流量分布不均问题呢&#xff1f;今天…

JavaScript 数组你都掰扯不明白,还敢说精通 JavaScript ?| 赠书

作者 | 哪吒来源 | CSDN博客最近小编在看文章的时候&#xff0c;总有很多刚刚入门的小白说精通这个&#xff0c;精通那个技术&#xff0c;更有意思的是&#xff0c;最近看到一则简历上说精通 JavaScript &#xff0c;聊一聊发现数组还不明白&#xff0c;就对外说精通~所以今天小…

php同时删除两个列表数据库,PHP 处理 数据库多表,既能高效又能思路清晰如何处理的?...

【情况如下】在表1中 记录着自行车编号 租户商家1 a2 a3 b4 c[这里的意思是&#xff0c;不同的商家。拥有的自行车&#xff0c;商家拥有自行车表]表2自行车编号 租出时间 租出金额1 2 41 3 51 6 102 3 53 4 7[这里 意味着。不同的商家 对自己的自行车 租出时间不同设置的价格&a…

基于消息队列 RocketMQ 的大型分布式应用上云实践

简介&#xff1a; Apache RocketMQ 作为阿里巴巴开源的支撑万亿级数据洪峰的分布式消息中间件&#xff0c;在众多行业广泛应用。在选型过程中&#xff0c;开发者一定会关注开源版与商业版的业务价值对比。 那么&#xff0c;今天就围绕着商业版本的消息队列 RocketMQ和开源版本 …

Gartner发布2022年政府行业主要技术趋势:XaaS、数字化、超自动化等

作者 | Gartner研究副总裁 Bettina Tratz-Ryan Gartner杰出研究副总裁John Kost Gartner高级研究总监 相斌斌 供稿 | Gartner 政府领导人和民选官员在2022年不仅要面对巨大的挑战&#xff0c;还要把握疫情与经济复苏应对措施、不断变化的政治需求和持续数字化变革所带来的机遇…

php include无效,php 两次include后,第一个include里的变量无效了

php 目录结构 rootindex.phpconfig.phpcindex.phproot/config.php 里的内容回复讨论(解决方案)首先,解析顺序如下1.文件路径是绝对路径?是, 包含, 解析结束。不是, 进入下一步。2.文件路径是相对路径(就像你的"../index.php")?是, 跳过include_path, 解析相对路径。…

RedShift到MaxCompute迁移实践指导

简介&#xff1a; 本文主要介绍Amazon Redshift如何迁移到MaxCompute&#xff0c;主要从语法对比和数据迁移两方面介绍&#xff0c;由于Amazon Redshift和MaxCompute存在语法差异&#xff0c;这篇文章讲解了一下语法差异 1.概要 本文档详细介绍了Redshift和MaxCompute之间SQL…

冲击中国超融合第一,浪潮的底气从何而来?

2016年被业内很多人称为“超融合元年”&#xff0c;一众存储、服务器、网络、安全厂商如同“摔杯为号”一般&#xff0c;齐齐迈入了超融合市场。于是&#xff0c;风助火势&#xff0c;火借风威&#xff0c;那年的“超融合”烧得正热。六年后的今天&#xff0c;当我们再度将目光…

数字农业WMS库存操作重构及思考

简介&#xff1a; 数字农业库存管理系统在2020年时&#xff0c;部门对产地仓生鲜水果生产加工数字化的背景下应运而生。项目一期的数农WMS中的各类库存操作均为单独编写。而伴随着后续的不断迭代&#xff0c;这些库存操作间慢慢积累了大量的共性逻辑&#xff1a;如参数校验、幂…

three.js和php,threejs--初创项目

一.环境介绍倘若你只是使用Three.js库中所提供的几何体&#xff0c;且不载入任何纹理贴图&#xff0c;则网页是可以从本地的文件系统中打开&#xff0c;并且是能够直接运行的&#xff0c;只需在文件管理器中双击HTML文件&#xff0c;它就可以在浏览器中进行显示。 (此时你将在地…

数字营销行业大数据平台云原生升级实战

简介&#xff1a; 加和科技CTO 王可攀&#xff1a;技术是为业务价值而服务 王可攀 加和科技CTO 本文将基于加和科技大数据平台升级过程中面临的问题和挑战、如何调整数据平台架构以及调整后的变化&#xff0c;为大家介绍数字营销行业大数据平台云原生升级实战经验。主要分为以…

云上更安全?亚马逊云科技宣布将持续加大在中国区域安全合规领域投入

编辑 | 宋慧 出品 | CSDN云计算 新冠疫情对我们工作产生了深远的影响&#xff0c;远程在线的工作与交流愈加普及&#xff0c;国内更多公司在推出居家办公的“混合办公”模式。不过&#xff0c;这也给了网络攻击更多的机会&#xff0c;CSDN看到&#xff0c;有多个安全领域的报告…

场景模型驱动自动化测试在盒马的探索及实践

简介&#xff1a; 盒马业务有如下几个特点&#xff1a;线上线下一体化、仓储配送一体化、超市餐饮一体化、经营作业一体化、多业态与平台化。在以上的种种原因&#xff0c;生鲜及物流体验是盒马的特点&#xff0c;但仓储配送一体化作业中&#xff0c;如何能更高效的提升测试效率…

基于 KubeVela 的 GitOps 交付

简介&#xff1a; KubeVela 是一个简单、易用、且高可扩展的云原生应用管理和交付平台&#xff0c;KubeVela 背后的 OAM 模型天然解决了应用构建过程中对复杂资源的组合、编排等管理问题&#xff0c;同时也将后期的运维策略模型化&#xff0c;这意味着 KubeVela 可以结合 GitOp…

php curlopt_postfields,PHP的CURLOPT_POSTFIELDS参数使用数组和字符串的区别 - CSDN博客

PHP的CURL组件是非常常用的HTTP请求模拟器。通常要发送post数据时&#xff0c;我已经习惯于这样写&#xff1a;curl_setopt( $ch, CURLOPT_POSTFIELDS,$post_data);但是在向某一个服务器发送请求的时候&#xff0c;服务器返回500。而使用socket方式连接上去发送请求&#xff0c…

BCS2022大会将提前至5月 网络安全产业空间扩容将成热门话题

年度网络安全的盛会即将开启。 2022年3月30日&#xff0c;2022年北京网络安全大会&#xff08;BCS2022&#xff09;新闻发布会在北京奇安信安全中心召开&#xff0c;宣布2022年北京网络安全大会“提档”至5月24日至26日&#xff0c;并与北辰集团国家会议中心达成战略合作&#…

基于 Istio 的全链路灰度方案探索和实践

简介&#xff1a; 本文介绍的基于“流量打标”和“按标路由” 能力是一个通用方案&#xff0c;基于此可以较好地解决测试环境治理、线上全链路灰度发布等相关问题&#xff0c;基于服务网格技术做到与开发语言无关。同时&#xff0c;该方案适应于不同的7层协议&#xff0c;当前已…

图像检索在高德地图POI数据生产中的应用

简介&#xff1a; 高德通过自有海量的图像源&#xff0c;来保证现实世界的每一个新增的POI及时制作成数据。在较短时间间隔内&#xff08;小于月度&#xff09;&#xff0c;同一个地方的POI 的变化量是很低的。 作者 | 灵笼、怀迩 来源 | 阿里技术公众号 一 背景 POI 是 Poin…

Redis HyperLogLog 是什么?这些场景使用它~

作者 | 就是码哥呀来源 | 码哥字节在移动互联网的业务场景中&#xff0c;数据量很大&#xff0c;我们需要保存这样的信息&#xff1a;一个 key 关联了一个数据集合&#xff0c;同时对这个数据集合做统计。统计一个 APP 的日活、月活数&#xff1b;统计一个页面的每天被多少个不…