openKylin论坛

 找回密码

erlang和其他语言读文件性能大比拼 [复制链接]

本帖最后由 lenky0401 于 2013-8-30 06:51 编辑

百岁同学说:

    今天公司技术比武,比赛题目是给一个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 filepen(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} = filepen(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) 1 [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 bywp-posturl pluginfor wordpress.

原文链接: http://blog.yufeng.info/archives/2842
楼主
发表于 2013-8-30 06:49:49
回复

使用道具 举报

openKylin

GMT+8, 2024-5-14 15:23 , Processed in 0.019368 second(s), 17 queries , Gzip On.

Copyright ©2022 openKylin. All Rights Reserved .

ICP No. 15002470-12 Tianjin

快速回复 返回顶部 返回列表