Home > Erlang探索, Linux, 调优 > erlang和其他语言读文件性能大比拼

erlang和其他语言读文件性能大比拼

August 28th, 2013

原创文章,转载请注明: 转载自系统技术非业余研究

本文链接地址: erlang和其他语言读文件性能大比拼

百岁同学说:

今天公司技术比武,比赛题目是给一个1.1g的大文本,统计文本中词频最高的前十个词。花了两天用erlang写完了代码,但是放到公司16核的机器上这么一跑,结果不比不知道,一比吓一条。erlang写的代码执行时间花了55秒左右,同事们有的用java,有的用C,还有的用C++,用C最快一个老兄只花了2.6秒,用java的也只用了3.2秒。相比之下erlang的代码,真是一头大蜗牛,太慢了。

详细参见这篇文章:http://www.iteye.com/topic/1131748

读取文件并且分析这是很多脚本语言如perl, python,ruby经常会干的事情.这个同学的问题是很普遍的问题,不只一个人反映过慢的问题。
今天我们来重新来修正下这个看法, 我们用数据说话。

首先我们来准备下文件, 这个文件是完全的随机数,有1G大小:

$ dd if=/dev/urandom  of=test.dat count=1024 bs=1024K
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB) copied, 188.474 s, 5.7 MB/s
$ time dd if=test.dat of=/dev/null 
2097152+0 records in
2097152+0 records out
1073741824 bytes (1.1 GB) copied, 1.16021 s, 925 MB/s

real    0m1.162s
user    0m0.219s
sys     0m0.941s
$ time dd if=test.dat of=/dev/null bs=1024k
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB) copied, 0.264298 s, 4.1 GB/s

real    0m0.266s
user    0m0.000s
sys     0m0.267s

我们准备了1G大小左右的文件,由于用的是buffered io, 数据在准备好了后,全部缓存在pagecache里面,只要内存足够,这个测试的性能和IO设备无关。 我们试着用dd读取这个文件,如果块大小是4K的话,读取这个文件花了1.16秒,而如果块大小是1M的话,0.26秒,带宽达到4.1GB每秒,远超过真实设备的速度。

那么我们用erlang来读取下这个文件来比较下,我们有三种读法:
1. 一下子读取整个1G文件。
2. 一个线程一次读取1块,比如1M大小,直到读完。
3. 多个线程读取,每个读取一大段,每次读取1M块大小。
然后比较下性能。

首先普及下背景:
1. erlang的文件IO操作由efile driver来提高,这个driver内部有个线程池,大小由+A 参数控制,所以IO是多线程完成的。
2. erlang的文件分二种模式: 1. raw模式 2. io模式 在raw模式下,数据直接由driver提供给调用进程, io模式下数据先经过file_server做格式化,然后再给调用进程。
3. 数据可以以binary和list方式返回,list方式下文件内容的byte就是一个整数,在64位机器上占用8个字节内存。

我们编写程序的时候要注意上面几点:

$ cat >rf.erl
-module(rf).
-compile(export_all).
-include_lib("kernel/include/file.hrl").

read(Filename)-> read(Filename, 1024 * 1024).

read(File, Bs)->  
    case file:open(File, [raw, binary]) of  
        {ok, Fd} ->  
            scan_file(Fd, file:read(Fd, Bs), Bs);  
        {error, _} = E ->  
            E
    end .  
  
scan_file(Fd, {ok, _Binary}, Bs) ->  
    scan_file(Fd, file:read(Fd, Bs), Bs);  
scan_file(Fd, eof, _Bs) ->  
    file:close(Fd);  
scan_file(Fd, {error, _}, _Bs) ->  
    file:close(Fd).  

read1(Filename) ->  
    {ok, _Binary} = file:read_file(Filename),  
    ok.  

upmap(F, L) -> 
    Parent = self(), 
    Ref = make_ref(), 
    [receive {Ref, Result} -> Result end 
     || _ <- [spawn(fun() -> Parent ! {Ref, F(X)} end) || X <- L]]. 

read2(Filename)->
    PoolSize = erlang:system_info(thread_pool_size),
    read2(Filename, PoolSize).

read2(_, 0)->
    io:format("setting +A first");
read2(Filename, PoolSize)->
    {ok, FInfo} = file:read_file_info(Filename),
    Bs = FInfo#file_info.size div PoolSize,
    erlang:display([{bs, Bs}, {poolsize, PoolSize}]),
    upmap(fun (Off)-> 
          {ok, Fd} = file:open(Filename, [raw, binary]),
          {ok, _} = file:pread(Fd, Off * Bs, Bs), 
          file:close(Fd),
          erlang:display([reading, block, Off * Bs, Bs, done]),
          ok 
          end, lists:seq(0, PoolSize - 1)),
    ok.
CTRL+D
$ erlc rf.erl

我们导出了三个读,分别对应着上面的3种方式,代码read,read1由帖子的作者的代码稍微修改来的,read2是我自己写的。

我们来测试下文件读取具体的性能:

$ erl +A 16  
Erlang R15B03 (erts-5.9.3.1) [source] [64-bit] [smp:16:16] [async-threads:16] [hipe] [kernel-poll:false]

Eshell V5.9.3.1  (abort with ^G)
1> timer:tc(rf, read, ["test.dat"]).
{322366,ok}
2> timer:tc(rf, read1, ["test.dat"]).
{779240,ok}
3> timer:tc(rf, read2, ["test.dat"]).
[{bs,67108864},{poolsize,16}]
[reading,block,603979776,67108864,done]
[reading,block,402653184,67108864,done]
[reading,block,939524096,67108864,done]
[reading,block,0,67108864,done]
[reading,block,805306368,67108864,done]
[reading,block,268435456,67108864,done]
[reading,block,469762048,67108864,done]
[reading,block,1006632960,67108864,done]
[reading,block,671088640,67108864,done]
[reading,block,134217728,67108864,done]
[reading,block,335544320,67108864,done]
[reading,block,67108864,67108864,done]
[reading,block,872415232,67108864,done]
[reading,block,738197504,67108864,done]
[reading,block,536870912,67108864,done]
[reading,block,201326592,67108864,done]
{214904,ok}
4> timer:tc(rf, read, ["test.dat", 1024]). 
{19826104,ok}
5> timer:tc(rf, read, ["test.dat", 4096]).
{3563313,ok}

我们采用的块大小是1M, 三种模式下对应的读取时间分别是0.322, 0.779, 0.214s,相比dd的0.264s, 我们可以看到多线程模式比c还快,单线程一次读和c差不多。带宽达到4-5G,是理论值的极限,也证明我们把这个事情做到极致了。

同时我们也看到了,如果1次读1K的话,就悲剧了,19秒,很多人会犯的错误。

同样的以4K块读,dd花了1.16秒,而Erlang花了3.56秒, 读完文件循环的次数是1G/4K=25万次。 任何细小的差别放大25万次都会很明显。

结论是: erlang的io是薄薄的一层c封装,每个file:read或者pread的时候,会把读写的具体参数发给driver, 然后等待driver发消息,返回IO结果。 每个io操作会涉及: 发消息+driver做IO操作+等消息 三个阶段。 所以如果我们的io操作太小,发消息和等消息的代价就会大,违反erlang的”小消息,大计算”的设计理念,低性能是一定的。

每个语言都有自己的特点,erlang同样有自己的惯用法。在io上,erlang性能是很高的,那么多的数据库系统是erlang写的也是佐证。

祝玩得开心!

Post Footer automatically generated by wp-posturl plugin for wordpress.

  1. 子翊
    August 28th, 2013 at 21:18 | #1

    霸爷 威武啊~~~~

  2. August 30th, 2013 at 16:46 | #2

    普通的 虚拟机 语言 基本就是 io 库函数的封装,距离 erlang 很大差距阿。

  3. fair_jm
    September 4th, 2013 at 10:29 | #3

    有点细节的东西不知道理解的对不对
    Bs = FInfo#file_info.size div PoolSize
    分块 然后后面由PoolSize个进程处理
    这样会不会文件有可能没被读完?? 是不是要PoolSize+1 个进程来读??

    Yu Feng Reply:

    你的理解是对的,没考虑不整除的情况.

    fair_jm Reply:

    嗯 嗯 霸爷的文章 受益良多^_^

    mmhajie Reply:

    lists:seq(0, PoolSize)

  4. September 15th, 2013 at 02:25 | #4

    [receive {Ref, Result} -> Result end || _ Parent ! {Ref, F(X)} end) || X <- L]].
    翻来覆去看了几遍才梳清楚逻辑,这算是erl行内惯用法么

    Yu Feng Reply:

    zuojiepeng Reply:

    [receive {Ref, Result} -> Result end || _ Parent ! {Ref, F(X)} end) || X <- L]].
    弱问第一个||的作用是什么?对erlang的列表推导出于入门级,只见过用一个||的,大牛解释一下~

    Yu Feng Reply:

    L= [spawn(fun() -> Parent ! {Ref, F(X)} end) || X <- L], [receive {Ref, Result} -> Result end || _ <-L]. 这样好看懂了吧!

    zuojiepeng Reply:

    哦,是根据第一层推导的列表元素个数,来起相应数量的receive,接收相应F的的消息。

  5. Heaven
    September 27th, 2013 at 22:55 | #5

    不知道理解的对不?read2并不能保证顺序读取?同时,想问下,如果在单机上面使用Erlang进行并行快速排序的话,是否效率不如快速排序?我自己写了一个并行的例子,感觉在一是得保证顺序接收排序好的队列,二是得进行大量的内存复制(基于消息传递),造成在我的电脑上500W随机数排序时,并行排序会挂掉。而快速排序没有压力。

  6. October 21st, 2013 at 16:24 | #6

    64位机器,内存有64G, 用您提供的这种方式,来读入处理一个2.4G的文本文件,大概流程是:
    1,10个线程,按照二进制方式读入
    2,读入之后把二进制转成list(binary_to_list) 3,进行常规字符串处理(tokens,nth方法)取出每行需要的字段。然后输出到标准输出(没有存储,直接io:format输出)。
    程序运行不完直接被kill了,应该是占用内存过多,top查看VIRT峰值达到了146G,RES峰值达到62G。
    用python测试2个指标的峰值都远远低于1G,erlang怎么占用这么多内存空间呢?

    mryufeng Reply:

    明显用错了呀,binary再转化成list. binary有完整的字符处理能力,binary模块,binary语法,还有bif,足够满足需求。

    mryufeng Reply:

    binary是最有效率的内存处理方式,接近C的效率了。

  7. kevin
    November 8th, 2013 at 14:14 | #7

    确实,很多吐槽erlang性能时,都是随便写了份代码,然后就说做什么的时候比Java慢,比Python慢的;
    还是应该这样认真针对这门语言的特性来编码。字符串处理时,binary转化成list确实没必要。
    很佩服霸爷研究erlang的时候经常翻看C代码的习惯,最近想深入了解下,erlang的虚拟机

    yang Reply:

    随便写了份代码不正常吗??别的语言是精心写的?。。大家都是随便写的。erlang能优化别的语言不能优化吗,最后结果不还是慢吗

    Yu Feng Reply:

    最后的结果是大家一样快,这个是我们想要的结果。

  8. yang
    December 3rd, 2013 at 14:03 | #8

    慢就是慢,没必要掩耳盗铃。批评才能让erlang进步。
    详细效率评测
    http://benchmarksgame.alioth.debian.org/u64q/benchmark.php?test=all&lang=hipe&lang2=java&data=u64q

    Yu Feng Reply:

    慢和没用对是二件事情。这里不是纠正用错的事情。

    Yu Feng Reply:

    这事情和jit(hipe)没半毛钱关系。

  9. botanyzh
    December 25th, 2013 at 22:09 | #9

    独立学习 兼页游项目实践 erlang 大半年了 从对霸爷的文章 难以看懂 不知所云 , 到现在 总想着突破瓶颈, 而时不时地关注霸爷这级别高手的博客, 每每时有所得 , 莫不令人快慰,

    叹我大天朝 在如此龌龊局面下 依然有 霸爷这样闪耀前行的领航人 ,着实令人鼓舞 感慨 。。。。
    真不知道我国还有多少实用领域 像计算机技术这样基本靠民间发展 坊间传承

    Yu Feng Reply:

    付出总会有回报

  10. Yuyou Chow
    March 11th, 2014 at 00:53 | #10

    [receive {Ref, Result} -> Result end || _ Parent ! {Ref, F(X)} end) || X <- L]].

    有个问题百思不得其解,这里用速构列表多次执行receive有什么特别的意义吗? 求解??

    Yu Feng Reply:

    速构列表在这里的左右代替for循环N次,因为对结果的接收没有顺序要求,所以写起来最优雅。

  11. fengwch
    January 4th, 2017 at 11:18 | #11

    这种多线程同时读取一个文件的指定位置的方式确实快,但是一般的需求是分行读取,逐行应用。

    请问:

    有什么方法可以快速分多线程同时读取一个文件的每行啊??

Comments are closed.