erlang和其他语言读文件性能大比拼
原创文章,转载请注明: 转载自系统技术非业余研究
本文链接地址: 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.
霸爷 威武啊~~~~
普通的 虚拟机 语言 基本就是 io 库函数的封装,距离 erlang 很大差距阿。
有点细节的东西不知道理解的对不对
Bs = FInfo#file_info.size div PoolSize
分块 然后后面由PoolSize个进程处理
这样会不会文件有可能没被读完?? 是不是要PoolSize+1 个进程来读??
Yu Feng Reply:
September 4th, 2013 at 10:31 am
你的理解是对的,没考虑不整除的情况.
fair_jm Reply:
September 4th, 2013 at 10:57 am
嗯 嗯 霸爷的文章 受益良多^_^
mmhajie Reply:
December 18th, 2014 at 6:09 pm
lists:seq(0, PoolSize)
[receive {Ref, Result} -> Result end || _ Parent ! {Ref, F(X)} end) || X <- L]].
翻来覆去看了几遍才梳清楚逻辑,这算是erl行内惯用法么
Yu Feng Reply:
September 22nd, 2013 at 4:31 am
是
zuojiepeng Reply:
October 18th, 2013 at 4:24 pm
[receive {Ref, Result} -> Result end || _ Parent ! {Ref, F(X)} end) || X <- L]].
弱问第一个||的作用是什么?对erlang的列表推导出于入门级,只见过用一个||的,大牛解释一下~
Yu Feng Reply:
October 18th, 2013 at 7:05 pm
L= [spawn(fun() -> Parent ! {Ref, F(X)} end) || X <- L], [receive {Ref, Result} -> Result end || _ <-L]. 这样好看懂了吧!
zuojiepeng Reply:
October 18th, 2013 at 9:57 pm
哦,是根据第一层推导的列表元素个数,来起相应数量的receive,接收相应F的的消息。
不知道理解的对不?read2并不能保证顺序读取?同时,想问下,如果在单机上面使用Erlang进行并行快速排序的话,是否效率不如快速排序?我自己写了一个并行的例子,感觉在一是得保证顺序接收排序好的队列,二是得进行大量的内存复制(基于消息传递),造成在我的电脑上500W随机数排序时,并行排序会挂掉。而快速排序没有压力。
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:
October 22nd, 2013 at 1:39 pm
明显用错了呀,binary再转化成list. binary有完整的字符处理能力,binary模块,binary语法,还有bif,足够满足需求。
mryufeng Reply:
October 22nd, 2013 at 1:40 pm
binary是最有效率的内存处理方式,接近C的效率了。
确实,很多吐槽erlang性能时,都是随便写了份代码,然后就说做什么的时候比Java慢,比Python慢的;
还是应该这样认真针对这门语言的特性来编码。字符串处理时,binary转化成list确实没必要。
很佩服霸爷研究erlang的时候经常翻看C代码的习惯,最近想深入了解下,erlang的虚拟机
yang Reply:
December 3rd, 2013 at 2:06 pm
随便写了份代码不正常吗??别的语言是精心写的?。。大家都是随便写的。erlang能优化别的语言不能优化吗,最后结果不还是慢吗
Yu Feng Reply:
December 3rd, 2013 at 4:18 pm
最后的结果是大家一样快,这个是我们想要的结果。
慢就是慢,没必要掩耳盗铃。批评才能让erlang进步。
详细效率评测
http://benchmarksgame.alioth.debian.org/u64q/benchmark.php?test=all&lang=hipe&lang2=java&data=u64q
Yu Feng Reply:
December 3rd, 2013 at 4:15 pm
慢和没用对是二件事情。这里不是纠正用错的事情。
Yu Feng Reply:
December 3rd, 2013 at 4:16 pm
这事情和jit(hipe)没半毛钱关系。
独立学习 兼页游项目实践 erlang 大半年了 从对霸爷的文章 难以看懂 不知所云 , 到现在 总想着突破瓶颈, 而时不时地关注霸爷这级别高手的博客, 每每时有所得 , 莫不令人快慰,
叹我大天朝 在如此龌龊局面下 依然有 霸爷这样闪耀前行的领航人 ,着实令人鼓舞 感慨 。。。。
真不知道我国还有多少实用领域 像计算机技术这样基本靠民间发展 坊间传承
Yu Feng Reply:
December 25th, 2013 at 11:32 pm
付出总会有回报
[receive {Ref, Result} -> Result end || _ Parent ! {Ref, F(X)} end) || X <- L]].
有个问题百思不得其解,这里用速构列表多次执行receive有什么特别的意义吗? 求解??
Yu Feng Reply:
March 11th, 2014 at 3:23 pm
速构列表在这里的左右代替for循环N次,因为对结果的接收没有顺序要求,所以写起来最优雅。
这种多线程同时读取一个文件的指定位置的方式确实快,但是一般的需求是分行读取,逐行应用。
请问:
有什么方法可以快速分多线程同时读取一个文件的每行啊??