单文件大数据求和方式对比

原文地址: Fastest way to sum integers in text file.
文末提供源码下载.

问题描述:

  假如你有一个很大的文本文件(1G大小), 里面有100,000,000 行数字.每个数字范围从0到1,000,000,000. 那么如何计算可以使计算时间最短?


方法1、逐行读取数字再累加。

private long sumLineByLine() throws NumberFormatException, IOException {
    BufferedReader br = new BufferedReader(new FileReader(file));
    String line;
    long total = 0;
    while ((line = br.readLine()) != null) {
        int k = Integer.parseInt(line);
        total += k;
    }
    br.close();
    return total;
}

这是最容易想到的一种处理方式. 题主的机器上运行了11次, 然后平均值为92.9 秒.

方法2、一点小改变。

经过评论的提醒, 题主想到可以在循环里减少局部变量来提高性能.

while ((line = br.readLine()) != null) {
    int k = Integer.parseInt(line);
    total += k;
}

改为

while ((line = br.readLine()) != null) {
    total += Integer.parseInt(line);
}

这一次的平均值为92.1 秒, 提高了1%. 好像不太乐观.

方法3、通过字符数组来解析数字

使用字符数组和位运算来解析数字

public long sumLineByLineManualParse() throws NumberFormatException,IOException {
        BufferedReader br = new BufferedReader(new FileReader(DataProcess.path));
        String line;
        long total = 0;
        while ((line = br.readLine()) != null) {
            char chs[] = line.toCharArray();
            int mul = 1;
            for (int i = chs.length - 1; i >= 0; i--) {
                char c = chs[i];
                switch (c) {
                case '0':
                    break;
                case '1':
                    total += mul;
                    break;
                case '2':
                    total += (mul << 1);
                    break;
                case '4':
                    total += (mul << 2);
                    break;
                case '8':
                    total += (mul << 3);
                    break;
                default:
                    total += (mul * ((byte) c - (byte) ('0')));
                }
                mul *= 10;
            }
        }
        br.close();
        return total;
    }

  题主本以为不用之前的parseInt方法,而改用基于字符的位运算会提高性能,但是没想到最后的结果反而更差。平均时间为148.2 秒。

方法4、基于字节流来处理(从后往前扫描)。

  缓存数组设置为8M大小,然后对每个字节进行数学计算。这种方式比较好理解:碰到第一个数字在个位,第二个数字在十位……,对应的加权因子为1,10……。

private long sumBinary() throws IOException {
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int lastRead = (int) raf.length();
    byte buf[] = new byte[8*1024*1024];
    int mul = 1;
    long total = 0;
    while (lastRead>0) {
        int len = Math.min(buf.length, lastRead);
        raf.seek(lastRead-len);
        raf.readFully(buf, 0, len);
        //计算剩余未读字节长度
        lastRead-=len;
        for (int i=len-1; i>=0; i--) {
            //48 is '0' and 57 is '9'
            if ((buf[i]>=48) && (buf[i]<=57)) {
                total+=mul*(buf[i]-48);
                mul*=10;
            } else
                mul=1;
        }
    }
    raf.close();
    return total;
}

  这一次的平均运行时间为30.8 秒! 题主很激动, 因为这比方法1足足提高了三倍效率.
  做到这一步,题主做了大量的思考,从各个角度思考为什么字节流的方式效率会这么高。甚至还想到了垃圾回收对字符流处理的影响,我真的对此佩服的五体投地,这需要多么扎实的java基础才能有如此丰富的联想啊。原文行文流畅,条理清晰,特别推荐大家去看。
  另外基于这种方法,题主还对缓存数组做了一点改造,将8M缓存改为了16K,计算时间也下降到23.7秒。题主对此的解释是:也许java的设计者们也做了类似的测试,所以才在16K的缓存时拥有较好的性能。

方法5、基于字节流来处理(从前往后扫描)

private long sumBinaryForward() throws IOException {
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int fileLength = (int) raf.length();
    byte buf[] = new byte[16 * 1024];
    int acc = 0;
    long total = 0;
    int read = 0;
    while (read < fileLength) {
        int len = Math.min(buf.length, fileLength - read);
        raf.readFully(buf, 0, len);
        read += len;
        for (int i = 0; i < len; i++) {
            if ((buf[i] >= 48) && (buf[i] <= 57))
                acc = acc * 10 + buf[i] - 48;
            else {
                total += acc;
                acc = 0;
            }
        }
    }
    raf.close();
    return total;
}

  这次的平均时间为20.0 秒,目前为止最快. 题主给的原因是方法5比方法4少用了一次乘法。那么题主思维又发散了,如果一次乘法都不用呢?还真让他找到了一种巧妙的方法,不过实验证明效率反而下降了。

private long sumBinaryCached() throws IOException {
    int mulCache[][] = new int[10][10];
    int coeff = 1;
    for (int i = 0; i < 10; i++) {
        for (int j = 0; j < 10; j++)
            mulCache[i][j] = coeff * j;
        coeff *= 10;
    }

    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int lastRead = (int) raf.length();
    byte buf[] = new byte[16 * 1024];
    int mul = 0;
    long total = 0;
    while (lastRead > 0) {
        int len = Math.min(buf.length, lastRead);
        raf.seek(lastRead - len);
        raf.readFully(buf, 0, len);
        lastRead -= len;
        for (int i = len - 1; i >= 0; i--) {
            if ((buf[i] >= 48) && (buf[i] <= 57))
                total += mulCache[mul++][buf[i] - 48];
            else
                mul = 0;
        }
    }
    raf.close();
    return total;
}

  至于原因?也许数组寻址不比一次乘法的代价小吧。

方法6、使用MappedByteBuffer

  接下来使用MappedByteBuffer来代替RandomAccessFile进行字节操作,看看效率是否有提高。

private long sumBinaryForwardMap() throws IOException {
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    byte buf[] = new byte[16 * 1024];
    final FileChannel ch = raf.getChannel();
    int fileLength = (int) ch.size();
    final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, 0,
            fileLength);
    int acc = 0;
    long total = 0;
    while (mb.hasRemaining()) {
        int len = Math.min(mb.remaining(), buf.length);
        mb.get(buf, 0, len);
        for (int i = 0; i < len; i++)
            if ((buf[i] >= 48) && (buf[i] <= 57))
                acc = acc * 10 + buf[i] - 48;
            else {
                total += acc;
                acc = 0;
            }
    }
    ch.close();
    raf.close();
    return total;
}

  平均时间为19.0 秒,比之前又有5%的提升。

方法7、使用多线程

  这个方法实现起来比较复杂,有两个问题要解决:①在使用字节流处理的时候,因为无法保证最后一个字节是空字符。所以如何保证数字字符的连续性。②如何把子线程的计算结果汇总起来。
  题主用到了分治法,使用的是Fork/Join框架,从前向后扫描文件并且严格保证执行顺序。入口方法如下:

private long sumBinaryForwardMapForked() throws IOException {
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    ForkJoinPool pool = new ForkJoinPool();

    byte buf[] = new byte[1 * 1024 * 1024];
    final FileChannel ch = raf.getChannel();
    int fileLength = (int) ch.size();
    final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, 0,
            fileLength);
    SumTaskResult result = new SumTaskResult();
    while (mb.hasRemaining()) {
        int len = Math.min(mb.remaining(), buf.length);
        mb.get(buf, 0, len);
        SumForkTask task = new SumForkTask(buf, 0, len);
        result.append(pool.invoke(task));
    }
    ch.close();
    raf.close();
    pool.shutdown();
    return result.subtotal;
}

  其他部分可以参考原文或者我的可测试代码

PS

在方法4,5,6中, 有48和57两个魔法数, 如果使用常量来代替48和57会严重的影响性能。暂时不清楚原因。

源码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,651评论 18 139
  • 这个城市 今天迎来了它的初雪 天空中的雪花 让我想起了那年的冬天 我们也在夜色下 窃窃私语 这个城市 这个冬天 我...
    Kiko安溥阅读 263评论 0 1
  • 亘古至今,沧海变桑田。多少人物事,面目全非。 有道是,世间唯一不变的,就是变。 有些事,变得好,变得妙。如陶变瓷,...
    烟花英雄阅读 591评论 1 1
  • 距离上次去泰国已经过去三个月时间,现在想想需要写些东西,一方面是简述泰国之旅的情况,一方面给打算出行的人些建议,有...
    呆呆吼阅读 429评论 0 1
  • [数据可视化] 本文编译自:Ross Crooks 数据是非常强大的。当然,如果你能真正理解它想告诉你的内容,那它...
    CoverUER阅读 125,162评论 0 20