您当前的位置: 首页 > 

终端卡顿优化的全记录

阿里云云栖号 发布时间:2021-11-08 10:44:18 ,浏览量:4

简介: 目前手机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

关注
打赏
1688896170
查看更多评论

阿里云云栖号

暂无认证

  • 4浏览

    0关注

    4522博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文
立即登录/注册

微信扫码登录

0.2498s