zoukankan      html  css  js  c++  java
  • Apache POI Java读取100万行Excel性能优化:split vs indexOf+subString,谁性能好

    使用Apache POI eventmodel实现一个Excel流式读取类,目标是100万行,每行46列,文件大小152MB的Excel文件能在20s读取并处理完。一开始实现的程序需要260s,离目标差太远了,使用jvisualvm分析各方法执行时间,结果如下:

    可以看到,程序中的splitLine和getRowNum方法消耗了大量时间。这两个方法都特别简单。splitLine方法将类似“123==hello”这样的字符串分解成{"123","hello"}数组,使用了String.split方法,getRowNum从Excel单元格地址字符串(比如“AB123456”)中获取行号“123456”,以下是原始实现方法:

    private String getRowNum(String cellRef){
        if(cellRef == null || cellRef == ""){
            return "-1";
        }
        
        String[] nums = cellRef.split("\D+");
        if(nums.length > 1){
            return nums[1];
        }
        return "-1;
    }
    
    private String[] splitLine(String line){
        return line.split("==");
    }

    两个如此简单的方法却消耗了这么多时间,一时间不知如何优化。最后突然想到:split的性能是否最优呢?对于如此简单的字符串分割,使用indexOf + subString性能如何呢?于是,我做了如下的实验:

    public static void main(String[] args) throws ParseException{
        String str = "AB123456";
        long start = System.currentTimeMillis();
        for(int i = 0 ; i < 10 * 10000 ; i ++){
            String[] lines = str.split("\D+");
        }
        long end = System.currentTimeMillis();
        System.out.println("split time consumed:" + (end - start) / 1000.0 + "s");
        
        start = System.currentTimeMillis();
        int index = -1;
        for(int i = 0 ; i < 10 * 10000 ; i ++){
            index = -1;
            for(int k = 0 ; k < str.length() ; k ++){
                if(str.charAt(k) >= '0' && str.charAt(k) <= '9'){
                    index = k;
                    break;
                }
            }
            
            if(index > 0){
                String[] lines = new String[]{str.substring(0, index),str.substring(index)};
            }
        }
        end = System.currentTimeMillis();
        System.out.println("indexof time consumed:" + (end - start) / 1000.0 + "s");
    }

    以下是输出结果:
    split time consumed:0.104s
    indexof time consumed:0.007s

    虽然表面上看,split比index + subString要简单很多,但后者性能是前者的将近15倍。用这种方法改写前面的splitLine和getRowNum,代码如下:

    private String getRowNum(String cellRef){
        int index = -1;
        for(int k = 0 ; k < cellRef.length() ; k ++){
            if(cellRef.charAt(k) >= '0' && cellRef.charAt(k) <= '9'){
                index = k;
                break;
            }
        }
        
        if(index >= 0){
            String[] nums = new String[]{cellRef.substring(0, index),cellRef.substring(index)};
            if(nums.length > 1){
                return nums[1];
            }
        }
        
        return "-1";
    }
    
    private String[] splitLine(String line){
        int index = line.indexOf("==");
        
        if(index > 0){
            return new String[]{line.substring(0, index),line.substring(index + 2)};
        }
        
        return new String[0]; 
    }

    优化后再用jvisualvm测试各方法执行时间:

    可以看到,我自己的数据处理方法已不是明显的性能瓶颈,而Apache POI的zip解压和文件读取占用了绝大部分时间。整体时间也从260s下降到了160s,已有了明显的提高。

    我们知道indexOf就是暴力搜索,split内部使用正则表达式做匹配,在搜索字符串较简单时肯定是indexOf性能好。大多数情况下调用split时都用不到正则表达式的那些高大上功能,所以完全没必要图方便在任何时候都用split,而是有所取舍:当简单分割字符串时自己用indexOf实现split,而涉及到复杂的分割操作,不得不用正则表达式时,才用split。为了看清String.split方法在做什么,我们看看JDK中String.split的源码:

        public String[] split(String regex, int limit) {
            /* fastpath if the regex is a
             (1)one-char String and this character is not one of the
                RegEx's meta characters ".$|()[{^?*+\", or
             (2)two-char String and the first char is the backslash and
                the second is not the ascii digit or ascii letter.
             */
            char ch = 0;
            if (((regex.value.length == 1 &&
                 ".$|()[{^?*+\".indexOf(ch = regex.charAt(0)) == -1) ||
                 (regex.length() == 2 &&
                  regex.charAt(0) == '\' &&
                  (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
                  ((ch-'a')|('z'-ch)) < 0 &&
                  ((ch-'A')|('Z'-ch)) < 0)) &&
                (ch < Character.MIN_HIGH_SURROGATE ||
                 ch > Character.MAX_LOW_SURROGATE))
            {
                int off = 0;
                int next = 0;
                boolean limited = limit > 0;
                ArrayList<String> list = new ArrayList<>();
                while ((next = indexOf(ch, off)) != -1) {
                    if (!limited || list.size() < limit - 1) {
                        list.add(substring(off, next));
                        off = next + 1;
                    } else {    // last one
                        //assert (list.size() == limit - 1);
                        list.add(substring(off, value.length));
                        off = value.length;
                        break;
                    }
                }
                // If no match was found, return this
                if (off == 0)
                    return new String[]{this};
    
                // Add remaining segment
                if (!limited || list.size() < limit)
                    list.add(substring(off, value.length));
    
                // Construct result
                int resultSize = list.size();
                if (limit == 0)
                    while (resultSize > 0 && list.get(resultSize - 1).length() == 0)
                        resultSize--;
                String[] result = new String[resultSize];
                return list.subList(0, resultSize).toArray(result);
            }
            return Pattern.compile(regex).split(this, limit);
        }

    尽管split方法的实现还是挺优化的,但仍做了太多的操作。

    想一想我过去写的代码经常图方便滥用String.split,这样是经不起大数据量考验的,学了这么长时间Java,竟从没想过这样的问题,不禁感叹自己还是菜鸟。虽然像Java或C#这种语言各种方法使用起来方便,但其库方法之下隐藏的性能开销,需要每一个使用者注意。

    (全文完)

  • 相关阅读:
    海龟交易
    暑假攻略:怎样让孩子过一个充实又省钱的假期
    值得追随
    在哪里能找的你想要的答案?
    顺势加仓策略
    交易中 你的加仓策略是怎样的?背后的逻辑是什么?
    驻守深寒:寻找那些有效地关键K线
    统计相关
    求助Ubuntu16.10如何设置默认启动为字符界面
    【Linux系列】Ubuntu ping通,xshell无法连接
  • 原文地址:https://www.cnblogs.com/ahhuiyang/p/3871918.html
Copyright © 2011-2022 走看看