从CPU的角度看代码优化

条件语句的优化

我们先来看条件语句的优化,我们知道,条件语句只有两种:if-else和switch。

if-else 语句的优化

根据上一章,我们知道 CPU 在遇到条件语句时,会执行跳转逻辑,那么,我们的优化点就是让 CPU 尽量不跳转,或者少跳转。

考察如下代码:

public String check(int age, int sex) {
    String msg = "";
    if(age > 18) {
        if(sex == 1) {
            msg = "符合条件";
        }else {
            msg = "不符合条件";
        }
    }else {
        msg = "不符合条件";
    }
    return msg;
}

逻辑很简单,就是筛选出age>18并且sex==1的人,代码一点儿问题都没有,但是太啰嗦,站在 CPU 的角度来看,需要执行两次跳转操作,当age>18时,就进入内层的if-else继续判断,也就意味着要再次跳转。

其实我们可以直接优化下这个逻辑,我们知道,逻辑与操作有个特点:全真才真,那就意味着,只要第一个不满足条件,后面的就不用看了。根据这个特点,我们就直接优化成如下代码:

public String check(int age, int sex) {
    String msg = "";
    if(age > 18 && sex == 1) {
        msg = "符合条件";
    }else {
        msg = "不符合条件";
    }
    return msg;
}

这样一来,只需要执行一次判断,也就是只需要执行一次跳转逻辑,就可以了,这就节省了 CPU 的力气;其实这不是最简单的,还可以更简化,比如:

public String check(int age, int sex) {
    if(age > 18 && sex == 1) return "符合条件";
    return "不符合条件";
}

这样是不是更好了,连else语句都省了,不仅效率提高了,而且连可读性也提高了。

其实,这些都可以总结为一点:逻辑能提前结束就提前结束。

比如,上述的两层if-else判断逻辑,因为只要有一个不符合就能直接提前结束,所以我们就使用逻辑与操作来提前结束。

多个判断的if-else逻辑,我们也可以优化,比如:

public String getPrice(int level) {
    if(level > 10) return 100;
    if(level > 9) return 80;
    if(level > 6) return 50;
    if(level > 1) return 20;
    return 10;
}

我们不用添加else分支,尽量提前结束即可,这样执行效率高,可读性也强。

switch 语句的优化

其实switch语句和if-else语句的区别不大,只不过写法不同而已,但是,switch语句有个特殊的优化点,那就是数组。

比如还是上述代码,我们改成 switch 语句:

public int getPrice(int level) {
    switch(level)
        case 10: return 100;
        case 9: return 80;
        case 8:
        case 7:
        case 6: return 50;
        case 5:
        case 4:
        case 3:
        case 2:
        case 1: return 20;
        default: return 10;
}

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

看着没啥区别,其实编译器会把它优化成一个数组,其中数组的下标为 0 到 10,不同下标对应的价格就是 return 的数值,也就是:

而我们又知道,数组是支持随机访问的,速度极快,所以,编译器对switch的这个优化就会大大提升程序的运行效率,这可比一条一条执行命令快多了。

那么,直接全部写switch不就行了?

不行,因为编译器对 switch 的优化是有条件的,它要求你的 code 必须是紧凑的。

紧凑是啥意思呢?必须小吗?不是,是连续的,比如:你的 code 是 1、50、51、101、110。这不是紧凑。 而你的 code 是 1000、1001、1002、1003。这就是紧凑。

这是为什么呢?因为我要用数组来优化你啊,你如果不是紧凑的,比如上述的最小 1,最大 110,我就要创建一个长度 110 的数组来存放你,而这么长的数组中,只有:1、50、51、101、110 这几个位置有用,岂不是浪费空间。

那这也不对啊,你那个 1000 开头的虽然紧凑,但是它大啊,你要创建一个长度为 1003 的数组呢。

但事实中,我不需要创建那么大,我只需要创建长度为 4 的数组就行了,下标分别为 0、1、2、3。计算的时候,我就用实际数字减去 1000,就得到对应的下标了,这就是个减法问题,效率是很高的。

所以,我们在使用switch的时候,尽量保证 code 是紧凑的,也就是连续的;也尽量使用数字类型的,因为你使用引用类型的话,它实际执行的 code 是引用类型的 hashcode,hashcode 是个 int 类型的数字,也不能保证是连续的。

循环语句的优化

其实循环语句跟条件语句很类似,只不过写法不同而已,循环语句的优化点也是以减少指令为主。

我们先来看一个中二的写法:

// 找出名字为张三的人
public User findUserByName(Users users) {
    User user = null;
    for(int i = 0; i < users.length; i++) {
        if(users[i].name.equals("张三")) {
            user = users[i];
        }
    }
    return user;
}

大哥,你说你这都找到人了,直接返回不行吗?还要接着遍历干啥?如果数组长度是 10086,第一个人就叫张三,那后面那 10085 次遍历不就白做了,真拿 CPU 不当人啊。

你直接这样写不就行了:

// 找出名字为张三的人
public User findUserByName(Users users) {
    for(int i = 0; i < users.length; i++) {
         // 找到了就趁早返回,别在那墨迹了
        if(users[i].name.equals("张三")) return user[i];
    }
    return null;
}

这样写效率高,可读性强,也符合我们上述的逻辑能提前结束就提前结束这个观点。

其实,这里还有一点可以优化的地方,就是我们的数组长度可以提取出来,不必每次都访问,也就是这样:

// 找出名字为张三的人
public User findUserByName(Users users) {
    // 将数组长度提取出来,不必每次循环都访问
    int length = users.length;
    for(int i = 0; i < length; i++) {
         // 找到了就趁早返回,别在那墨迹了
        if(users[i].name.equals("张三")) return user[i];
    }
    return null;
}

这看起来好像有点吹毛求疵了,确实是,但是如果考虑到性能的话,还是有点用的。比如有的集合的size()函数,不是简单的属性访问,而是每次都需要计算一次,这种场景就是一次很大的优化了,因为省了很多次函数调用的过程,也就是省了很多个call和return指令,这无异是提高了代码的效率的。尤其是在循环语句这种容易量变引起质变的情况下,差距就是从这个细节拉开的。

对于循环这种操作,我们要考虑的肯定是提前结束,越提前结束,效率越高。那么,遍历的时候,我们就要考虑一下遍历的方向了。

比如,我们要找一个年龄为 60 岁的人,而根据我们的经验,越早注册的用户,年龄越大,也就越早被添加到数据库中,也就越靠前,而 60 岁本来就是一个比较大的年龄,所以我们应该从前往后遍历,这样就能提前命中。而如果我们要找一个 20 岁的用户,则正好相反,就要从后往前遍历,这样才能更提前命中。

这些原因,还是那句话:逻辑能提前结束就提前结束。所以我们的工作重点就变成了:怎么让逻辑提前结束?

我们在执行循环的时候,尽量不要在循环体内创建变量,比如:

int sum;
for(int a = 0; a < 10; a++) {
    int b = a*2;
    sum +=b;
}

这每次循环,都创建一个 b,而前面我们讲过,局部变量的生命周期跟当前函数绑定,只要这个函数没调用完,它就一直存在,大大浪费内存,如果在循环体内创建对象,就更浪费了。

要是真需要的话,可以改为如下代码:

int sum;
int b;
for(int a = 0; a < 10; a++) {
    b = a*2;
    sum +=b;
}

这样只有一个变量,降低了内存使用率,也提升了代码的执行速度。

另外一个点就是,我们在遍历集合的时候,应该优先使用迭代器,这里面的原因就不多说了,大家去看一下相关的源码就明白了。

递归的优化

递归是一门伟大的发明,我们可以通过简单的函数调用,实现很复杂的逻辑。比如,求斐波那契数列的代码:

public int fib(int n) {
    if(n < 0) throw  new IllegalArgumentException("参数不合法");
    if(n == 0) return 0;
    if(n == 1) return 1;
    return fib(n-1) + fib(n-2); // 递归
}

代码很精彩,也没毛病,但是我们仔细想一下,假如n=10,流程如下:

  • f(10) = f(9) + f(8); //分别计算f(9)和f(8);

  • f(9) = f(8) + f(7); //分别计算f(8)和f(7),唉等等,上面我们计算过了f(8)啊,这里怎么还要计算;

  • f(8) = f(7) + f(6); //分别计算f(7)和f(6),又计算了一遍f(7)。

想必你已经看出问题了,我们做了好多次重复计算,这显然是不应该的。那么,我们能不能把这些重复的计算只做一次呢?

当然可以,不过我们就需要把重复计算的结果保存下来了,我们可以定义一个数组,将每个f(n)都保存下来,后面需要的时候直接去取就行了,修改后的代码如下:

// 修改后的代码,用一个数组保存计算过的结果
public int fib2(int n) {
    if (n == 0) return 0;
    int[] fibs = new int[n + 1]; // 用来保存从0到n的n个斐波那契数
    fibs[0] = 0; // 存0
    fibs[1] = 1; // 存1
    for (int i = 2; i <= n; i++) {
        fibs[i] = fibs[i - 1] + fibs[i - 2];// 这里不用递归计算了,直接用前面计算过的结果即可
    }
    return fibs[n];
}

改版后的代码不再是递归的了,而是采用一个数组缓存了计算的结果,后面的每个计算,直接去数组里获取即可。这种思维其实叫做动态规划(Dynamic Programming),简称为 DP。

那么,什么情况下我们的递归函数可以使用动态规划呢?

当我们的递归函数是运算类型的,并且里面有大量重复的运算的时候,就该想到采用动态规划。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值