小细节,大问题。分享一次代码优化的过程
Published on 2023-07-31 14:42 in 暂未分类 with null

小细节,大问题。分享一次代码优化的过程

    某个接口耗时大约8s,一开始我以为是io(主要是数据库)或者网络传输的瓶颈问题。
    想着多半是SQL优化的问题。
    接手一看,没有进行任何的IO操作或网络传输,仅仅是内存循环处理而已。
    我的开发电脑cpu是i7 8代,其运算能力,大概是,整数51.74GIPS,浮点43.99GFLOPS
    一个GFLOPS(gigaFLOPS)约等于每秒拾亿(=10^9)次的浮点运算
    好家伙,也就是一秒大约440亿次浮点运算?


    一般来说,现在的计算机,如果不是IO或网络瓶颈,你很难把一个接口整得很慢。


    需求不说了,极致简化以后大概的性能瓶颈是,需要对两个list进行嵌套循环(为什么要双循环,能不能移到外面?当前算法是基于这样,这是本文的前提,换一套算法那是另一个故事),
    伪代码

    for (int i = 0; i < list2.size(); i++) {
                for (int j = 0; j < list.size(); j++) {
                    list = list.stream().sorted(Comparator.comparing(e -> e.divide(BigDecimal.ONE))).collect(Collectors.toList());
                    // list.get(0)
                }
            }
    

    1. list.sort()和list.strem().sorted()排序的差异


    简单写了个demo
    List<Test.Obj> list = new ArrayList<>();
            Random random = new Random();
            for (int i = 0; i < 10000000; i++) {
                Test.Obj obj = new Test.Obj();
                obj.setNum(random.nextInt(10000) + 10);
                list.add(obj);
            }
    
            Collections.shuffle(list);
            long start = System.currentTimeMillis();
            //
            list.sort(Comparator.comparing(e -> e.getNum()/ 10));
            long end = System.currentTimeMillis();
    
    
            Collections.shuffle(list);
            long start2 = System.currentTimeMillis();
            //
            list = list.stream().sorted(Comparator.comparing(e -> e.getNum()/ 10)).collect(Collectors.toList());
            long end2 = System.currentTimeMillis();
    
            System.out.println(" 第1种耗时: " + (end - start) + " 第2种耗时: " + (end2 - start2));
    

    输出

     第1种耗时: 3601 第2种耗时: 6503
    

    大致可以得知list原生排序比stream()流效率要高。

    通过JMH做一下基准测试,分别测试集合大小在100,10000,100000时两种排序方式的性能差异。

    import org.openjdk.jmh.annotations.*;
    import org.openjdk.jmh.infra.Blackhole;
    import org.openjdk.jmh.results.format.ResultFormatType;
    import org.openjdk.jmh.runner.Runner;
    import org.openjdk.jmh.runner.RunnerException;
    import org.openjdk.jmh.runner.options.Options;
    import org.openjdk.jmh.runner.options.OptionsBuilder;
    
    import java.util.*;
    import java.util.concurrent.ThreadLocalRandom;
    import java.util.concurrent.TimeUnit;
    import java.util.stream.Collectors;
    
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    @Warmup(iterations = 2, time = 1)
    @Measurement(iterations = 5, time = 5)
    @Fork(1)
    @State(Scope.Thread)
    public class SortBenchmark {
    
        @Param(value = {"100", "10000", "100000"})
        private int operationSize; 
    
    
        private static List<Integer> arrayList;
    
        public static void main(String[] args) throws RunnerException {
            // 启动基准测试
            Options opt = new OptionsBuilder()
                    .include(SortBenchmark.class.getSimpleName()) 
                    .result("SortBenchmark.json")
                    .mode(Mode.All)
                    .resultFormat(ResultFormatType.JSON)
                    .build();
            new Runner(opt).run(); 
        }
    
        @Setup
        public void init() {
            arrayList = new ArrayList<>();
            Random random = new Random();
            for (int i = 0; i < operationSize; i++) {
                arrayList.add(random.nextInt(10000));
            }
        }
    
    
        @Benchmark
        public void sort(Blackhole blackhole) {
            arrayList.sort(Comparator.comparing(e -> e));
            blackhole.consume(arrayList);
        }
    
        @Benchmark
        public void streamSorted(Blackhole blackhole) {
            arrayList = arrayList.stream().sorted(Comparator.comparing(e -> e)).collect(Collectors.toList());
            blackhole.consume(arrayList);
        }
    
    }
    

    性能测试结果:

    差异还是非常明显的。


    还有一个非常大的问题在于,这里对list的排序仅仅只是为了获取排序字段最大值的那一列???

    你别说,你还真别说!
    好家伙,我直呼好家伙!

    我差点就给饶进去了!

    我们为什么不能只求极值? 求极值只需遍历。时间复杂度O(n)。

    而java list sort排序使用的是归并排序,平均时间复杂度:O(nlogn),只有在list本身已经完全有序的情况下(有病吗),才能达到最佳时间复杂度O(n)。

    优化方法:先统一使用list sort()排序,然后每次内部循环只求最大值所有列。

    PS: 这里有解释为什么list sort更快 为什么list.sort()比Stream().sorted()更快

    这两个小项改掉,响应时间直接砍半,来到了4-5秒。


    2. 别在计算列上进行排序

    以下代码分别对元素直接排序,和在排序时对元素进行计算并对结果排序。


    List<Integer> arrayList = new ArrayList<>();
            for (int i = 0; i < 10000000; i++) {
                arrayList.add(i + 100);
            }
            Collections.shuffle(arrayList);
            long start = System.currentTimeMillis();
            arrayList.sort(Comparator.comparing(e -> e));
            System.out.println(System.currentTimeMillis() - start);
    
            Collections.shuffle(arrayList);
            long start2 = System.currentTimeMillis();
    	int divisor = 2;
            arrayList.sort(Comparator.comparing(e -> e/divisor));
            System.out.println(System.currentTimeMillis() - start2);
    

    当divisor=2和1000时
    分别输出

    4897
    6499
    
    4797
    3383
    

    以下代码输出排序执行次数


    List<Integer> arrayList = new ArrayList<>();
            for (int i = 0; i < 10000000; i++) {
                arrayList.add(i + 100);
            }
    
            Collections.shuffle(arrayList);
            long start2 = System.currentTimeMillis();
            java.util.concurrent.atomic.AtomicInteger count = new AtomicInteger();
            int divisor = 2;
            arrayList.sort(Comparator.comparing(e -> {
                int i = e/divisor;
                count.getAndIncrement();
                return i;
            }));
            System.out.println("count " + count.get());
    

    当divisor=2和1000时
    count分别输出

    440496096
    278856902
    

    第一个输出440496096意味着e/divisor将被执行这么多次。其实它可以先遍历一次计算出来再排序,这样它就只需执行10000000次。
    第二个输出278856902表示,除数越大,结果就有很多相同的数,这本身代表着部份有序性。这可以减少大量的排序。


    优化方法:先统一将需要排序的值算出来,再进行排序。


    3. BigDecimal的精度与效率


    普通除法与BigDecimal除法的差异

    int elementCount = 10000000;
            List<Integer> arrayList = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(ArrayList::new));
            Collections.shuffle(arrayList);
            List<BigDecimal> list2 = new ArrayList<>(elementCount);
            for (int i = 0; i < elementCount ; i++) {
                list2.add(new BigDecimal(i));
            }
            Collections.shuffle(arrayList);
            Collections.shuffle(list2);
    
            long start = System.currentTimeMillis();
            for (int num : arrayList) {
                num = num / 10;
            }
            System.out.println(System.currentTimeMillis() - start);
    
            long start2 = System.currentTimeMillis();
            for (BigDecimal num : list2) {
                num = num.divide(new BigDecimal(10), 2, RoundingMode.HALF_UP);
            }
            System.out.println(System.currentTimeMillis() - start2);
    

    输出

    101
    497
    

    可以看到,BigDecimal除法和double/int数据类型的除法,前者耗时是后者的5倍左右。

    如果divide不设置精度num = num.divide(new BigDecimal(10))
    差异更大。

    99
    3677
    

    当然,这种生产环境肯定不会这样使用,除不尽会抛出异常。

    优化方法:这里不需要获取高精准度,所以这里改用double进行除法。除数是变量,这里没有使用位移。


    小结


    总之就是一些不起眼的小细节,在平常的时候其实无所谓。
    比如,假设一个场景,人员表分页查询返回前端最多100来条了,需要根据身份证号码计算年龄并排序,考虑到直接在SQL里计算可能使身份证唯一索引失效,拿到代码中计算并排序。

    userList = userList.stream().sorted(Comparator.comparing(e -> getAge(e.getIdcard()))).collect(Collectors.toList());
    

    100来条的数据量根本不需要去考虑,list.sort()和stream().sorted()的性能差异。
    以及是不是在排序列上进行了计算。
    甚至于我可能需要在某个列上进行BigDecimal的四则运算。又怎样?
    在这点数据量上又算得了什么呢?

    但如果不注意这些细节,刚好遇上了开头所说的这个场景,那这些小细节可能就会产生非常巨大的性能差异。


    通过以上3个改进点。一顿操作猛如虎,接口耗时从7-8秒稳定在了500-600毫秒。

    此算法框架下,基本满足了要求。
    更高的响应速度的话,基本就要从根上换一套算法了。

    posted @   是奉壹呀  阅读(3059)  评论(8编辑  收藏  举报
    相关博文:
    阅读排行:
    · 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
    · 没有源码,如何修改代码逻辑?
    · NetPad:一个.NET开源、跨平台的C#编辑器
    · PowerShell开发游戏 · 打蜜蜂
    · 在鹅厂做java开发是什么体验
    点击右上角即可分享
    微信分享提示