• 性能优化之代码优化


    背景:面试时我喜欢问候选人的一个问题是:是否有性能优化的经历与案例可以分享。大多数候选人一上来就说sql优化,甚至直接谈起如何建索引。诚然多数的性能问题是由于不合适的sql/索引引起,但是代码级别的优化,就真的没有可挖之处了吗?
    本文笔者将根据实际项目中碰到的部分案例浅析代码优化那点事

    1、Map实现Code2Name,减少时间复杂度
    案例:已有学生信息列表,班级信息列表,翻译每个学生(只知道班级ID,不知道班级名称)所在的班级名

    @Data
     public class Student {
            private int name;
            private int classId;
            //扩展属性
            private String className;
     }
    @Data
    private class ClassInfo{
        private int classId;
        private String ClassName;
    }
    
    public void translateClassName(List<Student> studentList, List<ClassInfo> classInfoList){
       for(Student student : studentList){
           for(ClassInfo classInfo : classInfoList){
               if(student.getClassId() == classInfo.getClassId()){
                   student.setClassName(classInfo.getClassName());
               }
            }
        }
    }
    

    改进后的代码如下:

    public void translateClassNameImprove(List<Student> studentList, List<ClassInfo> classInfoList){
        Map<Integer, String> classId2Name = new HashMap<>();
        for(ClassInfo classInfo : classInfoList){
            classId2Name.put(classInfo.getClassId(), classInfo.getClassName());
        }
        for(Student student : studentList){
           student.setClassName(classId2Name.get(student.getClassId()));
        }
    }
    

    相比之下,前者的时间复杂度是N*N,后者的时间复杂度接近2N。

    2、循环或多次字符串拼接
    循环或多次String变量相加,请使用StringBuilder,通过append()方法拼接生成字符串。每次字符串相加类似于new StringBuilder().append(a).append(b).append(c).toString(); 如果在循环中多次相加,相当于new 了很多次临时的StringBuilder对象。

    3、Log输出时字符串拼接
    当年使用log4j时有很多类似isInfoEnable()这种当前日志启用级别的判断,为的就是避免多余的字符串拼接操作
    示例:

    //假如当前的级别为ERROR,也会先进行a + b + c拼接运算
    logger.info(a + b + c);
    
    //假如当前的级别为ERROR,不会进行a + b + c拼接运算
    if(logger.isInfoEnable()){
        logger.info(a + b + c);
    }
    

    缺点显而易见,就是到处充斥着isInfoEnable()这种冗余代码,这个问题在Slf4j时已得到解决,例如:

    String url = “moext.com”;
    //假如当前的级别为ERROR,不会进行拼接运算
    logger.info(“article from url {}”, url);
    

    但是即使用了Slf4j,仍有不少开发的童鞋是这样写的:

    //假如当前的级别为ERROR,也会先进行a + b + c拼接运算
    logger.info(a + b + c);
    

    这样仍然有同样的问题。

    4、循环进行跨进程调用合并成批量操作
    例如,循环里调用数据库或redis查询不同用户信息,可以合并成一到N次批量查询
    循环里调用数据库或redis存储用户信息,可以合并成一到N次批量存储

    5、公共信息或高成本对象使用cache空间换时间
    即将复用程度很高的数据缓存,以空间换时间的常用手法。

    6、[可选] httpClient与URLConnection
    相比之下HttpClient更像是Http客户端操作,比如对cookie支持,对https支持,易用、通用、扩展性更高。而URLConnection更像是一个半成品,需要做定制化开发。
    但后者的性能更高,如果你的项目中只是简单的通过http调用一些其他的项目(可控的),而且对性能的损失也很在意,不妨考虑在URLConnection基础上做些少量封装。本条由于提高性能的同时,会降低扩展性,故实际根据项目需要可选。

    7、If判断时,将最常见的条件放最前面
    减少分支判断,让CPU以最快的速度命中条件。

    8、ArrayList这种内部采用元素采用数组类型的,条件允许的情况下尽量指定初始大小
    当元素数量达到默认size时,会进行扩容,重新分配一段更大的连续内存 ,涉及到旧对象的copy操作,代价不小。
    HashMap同理,所不同的时HashMap引入了加载因子,默认为0.75,即元素数量达到0.75 * 默认size时,会发生扩容,即使这样,指定大小也可以减少扩容次数,一定程度上提高了性能。

    9、事务过长
    尤其是使用了Spring默事务支持方式@Transactional,经常可以发现在事务方法范围里有其他复杂逻辑运算,甚至是数据库查询操作,优化方法为尽量只保持最简洁的事务操作代码在此方法里,将其余的查询及运算操作放到方法外。(需要注意的是,如果使用Spring默认的AOP事务,在是同一个类里方法调用子方法,而声明子方法为@Transactional,事务是不会生效的)

    10、多余对象
    某项目中的代码片段

    TDailyTask record = new TDailyTask();
    if (CollectionUtils.isEmpty(tDailyTaskList)) {
              record.setNote("文章来自http://moext.com");
              tDailyTaskMapper.insert(record);
    }
    

    如果if条件不成立,也new TDailyTask(),属于多余对象,改进后如下:

    if (CollectionUtils.isEmpty(tDailyTaskList)) {
              TDailyTask record = new TDailyTask();
              record.setNote("文章来自http://moext.com");
              tDailyTaskMapper.insert(record);
    }
    

    11、善用BufferedReader和BufferedWriter
    带缓存的输入输出流。一般情况下IO的成本是较高的,如果每次只读取或写入一个字符,那效率可想而知,而带缓冲的输入输出流则可以通过分批读取和写入字符提高效率。

    12、使用最有效率的方式去遍历HashMap

    Set<Object> keySet = map.keySet();
    for(Object key : keySet){
           Object value =  map.get(key);
           //do something...
    }
    

    循环里多了一次map.get(key)操作,最好情况下时间复杂度接近N,最坏接近N*N,最优遍历方式如下如下:

    Set<Map.Entry<Object, Object>> entrySet = map.entrySet();
        for(Map.Entry<Object, Object> entry : entrySet){
            Object key = entry.getKey();
            Object value = entry.getValue();
            //do something...
        }
    

    时间复杂度稳定为N
    12、慎用反射,如BeanUtils.copyProperties()
    BeanUtils提供对Java反射和自省API的包装。其主要目的是利用反射机制对JavaBean的属性进行处理。我知道在很多Java项目中PO对象和VO对象属性基本是相同的,开发童鞋为了减少get/set的书写,通过BeanUtils.copyProperties()来减少代码量,但这个方法的性能实在不高,对象属性越多则越明显。通常来说使用反射可以减少代码量,但对性能的损失也要有清醒的认识,折衷根据项目需要做出最优选择。
    13、对比之后选择json/xml序列化和反序列化框架
    不同的json和xml解析包的解析性能差距有时候相差甚远,请对比后做出选择。这里就不广告了

    14、大文件读与写
    大文件的“大”主要体现在(容易)超出内存容易限制,简单又好用的处理方式为按行处理,这就要求大文件定协议时要考虑按行处理的性能需求。

    15、ConcurrentHashMap
    Map的线程安全是很常见的需求,有用HashTable的,也有对HashMap进行包装后将方法加synchronized的。建议直接采用ConcurrentHashMap,其内部采用多“桶”机制,不同的key根据hash算法落到某个“桶”,理论上写并发能力提高了N倍 (N为桶数量),同时降低了读一致性。除非对强读写一致性有很严格的需求,否则ConcurrentHashMap是适用大多数场景的。

    欢迎转载,转载请务必注明出处
  • 相关阅读:
    《挑战30天C++入门极限》新手入门:关于C++中的内联函数(inline)
    《挑战30天C++入门极限》新手入门:C/C++中枚举类型(enum)
    《挑战30天C++入门极限》新手入门:C++中布尔类型
    《挑战30天C++入门极限》新手入门:C++下的引用类型
    《挑战30天C++入门极限》新手入门:C/C++中数组和指针类型的关系

    《挑战30天C++入门极限》入门教程:C++中的const限定修饰符
    《挑战30天C++入门极限》c++中指针学习的两个绝好例子
    《挑战30天C++入门极限》在c/c++中利用数组名作为函数参数传递排序和用指针进行排序的例子。
    《挑战30天C++入门极限》引言
  • 原文地址:https://www.cnblogs.com/mzsg/p/11977808.html
Copyright © 2020-2023  润新知