终端卡顿优化的全记录( 二 )


也就是说变量的存储应该按照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次晶体震荡周期才能完成

特别声明:本站内容均来自网友提供或互联网,仅供参考,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。