6
14
2018
3

递归遍历目录:Python vs Go vs Rust

群友提出了一个简单的任务:递归遍历一个很大的目录,根据文件名数一下有多少 JPEG 文件。怎么最快呢?然后他用了 Go 语言实现。

我忽略想起 Python 3.5 的 What's New 里提到,他们优化了 os.scandir 使得目录遍历快了好几倍(PEP 471)。其核心思想是:不进行不必要的 stat 系统调用,因为读目录获得了不少信息,原来都是丢弃掉了,现在改成了通过 DirEntry 对象来返回。这些信息包括文件名等,刚好有我们需要的。

于是 Go 做了这个优化没有呢?

翻了一下代码。Go 自带的实现位于 src/path/filepath/path.go 文件中。可以看到,它对每一个文件都 lstat 了。后来一阁指出,不仅如此,而且它还莫名其妙地对目录下的文件名进行了排序

呃,前者可以说是疏忽了,毕竟 Python 也是直到 3.5 才优化的。可是,它排那个序干嘛呢……

然后我又想到,Rust 那边如何呢?

结果是,Rust 对它所包含的东西非常审慎,标准库里并没有递归遍历目录的函数。那我们自己写一个?才不呢,用第三方库啦!可以看到,它也是返回 DirEntry 对象的。

后来了解到,Go 也有一个第三方的实现 godirwalk,对这些细节进行了优化。

光是了解实现不够。我们让它们来比试一下吧。顺便,把 find 和 fd 也拖进来好了。

任务:数一数一个拥有近万文件的目录下有多少 JPEG 文件。

实现代码:walkdir-test

结果:

   Rust: top:    4.78, min:    4.72, avg:    4.90, max:    5.46, mdev:    0.17, cnt:  20
 Go_3rd: top:    7.71, min:    7.64, avg:    7.79, max:    8.41, mdev:    0.16, cnt:  20
   find: top:   11.49, min:   11.32, avg:   11.76, max:   14.18, mdev:    0.59, cnt:  20
     fd: top:   18.17, min:   15.18, avg:   21.29, max:   29.94, mdev:    3.84, cnt:  20
     Go: top:   21.08, min:   20.91, avg:   21.28, max:   22.70, mdev:    0.37, cnt:  20
 Python: top:   29.66, min:   29.51, avg:   30.43, max:   35.84, mdev:    1.45, cnt:  20
Python2: top:   30.37, min:   30.10, avg:   30.85, max:   33.15, mdev:    0.75, cnt:  20

Rust 如预期一样是最快的。Go_3rd 就是那个第三方库的实现,也非常快的。fd 是 Rust 实现的,目标之一是快,但是这次并没有比老牌的 find 快。Go 自带的那个实现,十分令人遗憾地连 find 都没比过呢,不过还是比 Python 快了不少。Python 2 这次终于没有跑在 Python 3 前边了(虽然差距很小),我猜是 PEP 471 那个优化的功劳。

对了,还有代码行数:

  15 Python/walk
  29 Rust/src/main.rs
  30 Go/walk.go
  33 Go_3rd/walk.go

Rust 竟然不是最长的。不过确实是字符数最多的。

话说 Go 的 } 竟然也是有规定的,结构体的不能另起一行写,只能跟 Lisp 的风格那样堆在一行的尾巴里。

PS: 没想到之前给 swapview 写的 benchmark 程序在另外的项目里用上了呢,果然写东西还是通用些的好。


更新:在群友的提示下,我找了一个更大的目录来测试,结果很不一样呢。这次遍历的目录是 /usr,共有 320397 个文件。

     fd: top:  265.80, min:  259.84, avg:  273.89, max:  319.76, mdev:   15.03, cnt:  20
   Rust: top:  269.98, min:  266.86, avg:  272.82, max:  282.84, mdev:    4.17, cnt:  20
 Go_3rd: top:  361.17, min:  359.05, avg:  363.82, max:  370.22, mdev:    3.31, cnt:  20
   find: top:  454.03, min:  450.79, avg:  458.51, max:  467.31, mdev:    5.08, cnt:  20
 Python: top:  624.80, min:  615.73, avg:  630.67, max:  640.88, mdev:    6.79, cnt:  20
     Go: top:  890.03, min:  876.98, avg:  910.63, max:  967.14, mdev:   24.84, cnt:  20
Python2: top: 1171.38, min: 1157.19, avg: 1189.99, max: 1228.09, mdev: 4186.28, cnt:  20

可以看到,唯一的并行版本 fd 胜出了~Rust 版本紧随其后,显然在此例中并行并没有多么有效。Go_3rd 还是慢于 Rust 但也并不多。然后,经过优化的 Python 终于在更大的数据量上明显胜过了 Go 以及 Python 2 这两个浪费了很多系统调用的版本。

Category: 编程 | Tags: python go 编程语言 Rust
7
14
2017
12

swapview 更新

距离上一次 swapview 的更新已经一年多了。在这段时间里,不少语言有了比较大的更新,所以再跑一次。

首先是运行不了或者有问题的语言和实现:

  • Julia: 新版本不向后兼容,运行不了。求修
  • Nim: 标准库有些函数的行为有改变:walkFiles 不再返回目录文件,split 不再将连续的空白符作为一个分隔符。
  • Erlang: 不再支持 ~.0f 格式化字符串。

其中不向后兼容的,Julia 和 Nim 还没到达 1.0 版,所以坑人了也就坑了。Erlang 不知道是怎么回事。

然后是有警告的:

  • R: 文件打开失败有警告。不是大问题,不过有点烦。如果你知道怎么去掉它,请告诉我。
  • Elixir: 函数无参调用时不加括号会触发警告。看来 Elixir 也不喜欢 Ruby 函数调用不加操作的行为了呢。

还有发行版的锅:

  • CSharp: mono 与 chicken 冲突,无法安装,所以跑不了。
  • Haskell: Arch Linux 不再支持静态链接了。需要改一下编译参数。

我还对代码做了一些改进:

  • Rust_parallel: 用 rayon 换掉了 threadpool。rayon 更适合这种并行任务处理。另外稍微改进了一下代码。
  • NodeJS: 使用 ECMAScript 6 语法(箭头函数和 const / let 变量声明)。去掉不必要的分号。
  • C: 支持 Android 平台。
  • 修正了一些实现的格式化输出(还剩下一些)。

最后结果如下。因为 CPU 换成了 i7-7700HQ,所以耗时都比之前少了不少。另外注意,前排几名只有前三名都是多线程的,所以 Go_goroutine 比那些 C 和 C++ 版本快很正常。

           Rust_parallel: top:   30.48, min:   27.76, avg:   32.48, max:   37.80, mdev:    2.78, cnt:  20
               C++98_omp: top:   31.24, min:   29.04, avg:   34.42, max:   49.48, mdev:    4.52, cnt:  20
            Go_goroutine: top:   68.30, min:   61.87, avg:   75.89, max:  142.91, mdev:   16.39, cnt:  20
                   C++14: top:   83.17, min:   82.23, avg:   84.71, max:   92.58, mdev:    2.76, cnt:  20
             C++14_boost: top:   83.58, min:   83.20, avg:   84.58, max:   91.00, mdev:    1.72, cnt:  20
                   C++98: top:   83.71, min:   83.09, avg:   85.19, max:   91.48, mdev:    2.44, cnt:  20
                    Rust: top:   91.45, min:   90.81, avg:   93.08, max:   99.38, mdev:    2.07, cnt:  20
                       C: top:   91.49, min:   90.49, avg:   93.41, max:   99.44, mdev:    2.53, cnt:  20
                   C++11: top:   91.81, min:   91.33, avg:   93.52, max:  102.80, mdev:    3.04, cnt:  20
                     PHP: top:   93.91, min:   93.37, avg:   94.98, max:   99.42, mdev:    1.47, cnt:  20
                   OCaml: top:  106.85, min:  105.75, avg:  109.34, max:  118.03, mdev:    3.37, cnt:  20
                     Nim: top:  109.28, min:  108.44, avg:  110.75, max:  117.43, mdev:    2.13, cnt:  20
         D_parallel_llvm: top:  111.25, min:  109.43, avg:  113.21, max:  117.26, mdev:    2.33, cnt:  20
              D_parallel: top:  116.77, min:  114.69, avg:  118.95, max:  125.45, mdev:    2.87, cnt:  20
                    PyPy: top:  126.23, min:  124.29, avg:  128.34, max:  134.07, mdev:    2.79, cnt:  20
                  D_llvm: top:  129.63, min:  128.52, avg:  131.32, max:  137.65, mdev:    2.41, cnt:  20
                  LuaJIT: top:  132.68, min:  131.31, avg:  134.36, max:  143.07, mdev:    2.57, cnt:  20
                      Go: top:  135.57, min:  132.37, avg:  139.25, max:  148.37, mdev:    4.50, cnt:  20
                       D: top:  146.30, min:  145.00, avg:  149.14, max:  159.02, mdev:    3.85, cnt:  20
                Haskell2: top:  150.92, min:  149.41, avg:  153.25, max:  164.60, mdev:    3.53, cnt:  20
                 Python2: top:  155.36, min:  152.26, avg:  158.55, max:  170.20, mdev:    4.60, cnt:  20
                    Vala: top:  159.55, min:  157.87, avg:  161.40, max:  166.52, mdev:    2.26, cnt:  20
                  Erlang: top:  163.00, min:  158.63, avg:  168.76, max:  181.77, mdev:    7.09, cnt:  20
                   Lua51: top:  166.58, min:  164.58, avg:  168.89, max:  181.71, mdev:    3.69, cnt:  20
                   Lua52: top:  168.48, min:  167.40, avg:  170.82, max:  178.11, mdev:    3.36, cnt:  20
           Python3_bytes: top:  174.30, min:  172.65, avg:  176.83, max:  181.64, mdev:    2.91, cnt:  20
                   Lua53: top:  180.20, min:  177.79, avg:  185.01, max:  199.41, mdev:    6.07, cnt:  20
                    Perl: top:  180.22, min:  177.30, avg:  182.21, max:  186.09, mdev:    2.44, cnt:  20
              FreePascal: top:  180.85, min:  179.35, avg:  184.23, max:  197.83, mdev:    4.84, cnt:  20
                 Python3: top:  181.72, min:  178.47, avg:  184.09, max:  189.67, mdev:    2.99, cnt:  20
                    Ruby: top:  199.82, min:  197.16, avg:  203.62, max:  218.32, mdev:    4.92, cnt:  20
                 Chicken: top:  234.69, min:  232.11, avg:  239.61, max:  248.39, mdev:    5.63, cnt:  20
             PyPy3_bytes: top:  238.55, min:  237.18, avg:  242.08, max:  253.68, mdev:    4.53, cnt:  20
                   Guile: top:  254.49, min:  249.14, avg:  260.40, max:  275.83, mdev:    7.12, cnt:  20
              ChezScheme: top:  265.63, min:  262.52, avg:  268.56, max:  278.53, mdev:    3.94, cnt:  20
                    Java: top:  291.35, min:  283.94, avg:  302.36, max:  324.82, mdev:   12.38, cnt:  20
                  NodeJS: top:  317.01, min:  314.61, avg:  321.04, max:  332.05, mdev:    4.71, cnt:  20
                    Dart: top:  329.39, min:  325.63, avg:  334.57, max:  351.19, mdev:    6.92, cnt:  20
           Ruby_rubinius: top:  359.76, min:  357.74, avg:  363.13, max:  373.02, mdev:    4.45, cnt:  20
          CommonLisp_opt: top:  360.57, min:  358.41, avg:  365.15, max:  378.44, mdev:    5.76, cnt:  20
                     Tcl: top:  367.38, min:  363.28, avg:  372.89, max:  388.57, mdev:    6.65, cnt:  20
          CommonLisp_old: top:  376.27, min:  371.99, avg:  379.66, max:  390.55, mdev:    4.33, cnt:  20
                   PyPy3: top:  384.12, min:  376.60, avg:  390.16, max:  401.39, mdev:    7.32, cnt:  20
            CoffeeScript: top:  414.40, min:  393.13, avg:  432.25, max:  466.42, mdev:   20.64, cnt:  20
   CoffeeScript_parallel: top:  451.12, min:  425.11, avg:  464.92, max:  491.52, mdev:   17.05, cnt:  20
            NodeJS_async: top:  454.78, min:  437.13, avg:  465.18, max:  489.06, mdev:   13.02, cnt:  20
         Racket_compiled: top:  510.97, min:  505.22, avg:  516.20, max:  527.69, mdev:    6.23, cnt:  20
                  Racket: top:  520.70, min:  515.11, avg:  525.28, max:  533.79, mdev:    5.87, cnt:  20
         NodeJS_parallel: top:  673.38, min:  664.38, avg:  687.60, max:  724.04, mdev:   16.32, cnt:  20
                   Scala: top:  719.27, min:  698.23, avg:  740.32, max:  815.95, mdev:   27.27, cnt:  20
           Bash_parallel: top:  769.14, min:  751.56, avg:  775.91, max:  791.40, mdev:    8.82, cnt:  20
                 Haskell: top: 1036.33, min: 1013.27, avg: 1048.70, max: 1090.21, mdev: 4186.25, cnt:  20
                  Elixir: top: 1097.32, min: 1075.24, avg: 1113.36, max: 1144.80, mdev: 4186.26, cnt:  20
                       R: top: 1141.37, min: 1120.69, avg: 1156.42, max: 1177.79, mdev: 4186.26, cnt:  20
                    Bash: top: 1368.00, min: 1323.22, avg: 1479.66, max: 1994.19, mdev: 4077.71, cnt:  20
              POSIX_dash: top: 1841.09, min: 1833.25, avg: 1851.09, max: 1881.68, mdev: 3897.64, cnt:  17
               POSIX_zsh: top: 2124.79, min: 2110.81, avg: 2134.32, max: 2156.40, mdev: 3841.56, cnt:  15
              POSIX_bash: top: 2200.64, min: 2195.09, avg: 2206.75, max: 2221.41, mdev: 3807.09, cnt:  14
                  CSharp: FAILED with entity not found
                   Julia: FAILED with entity not found

对比旧结果,可以看到有一些比较大的变化:

Rust 快了不少,并行版一跃成为最快的实现。C++98 OpenMP 版紧随其后。Rust 单线程版也上升了四名,与 C、C++ 版本接近,并超越了所有的 D 实现。Go 并行版也提升了不少,位居第三,但它花费的时间比前两名所花费时间的总和还要多……并且结果也不是很稳定(标准差比前二十名都要大不少)。

Nim 慢了不少,可能是因为没字符串分割函数可用,我改用了 pegs。这东西很慢的样子,也许正则还会快一点……C 也落后了一些,但是与 C++ 版本的差距不大。Haskell 大概是因为改用动态链接的原因,慢了少许。

PyPy 快了很多,竟然超越了 LuaJIT。Erlang、Guile、Rubinius 也都大幅上升,而 NodeJS 不知道怎么了,全面落后于 Python、Ruby、Lua。PHP 更新到 7 之后依旧非常非常快。

完整的排名变化可以看这里

Category: 编程 | Tags: go 编程语言 Rust
3
10
2017
9

在 Vim 8 里用 git subrepo 安装 vim-go

我一直没有使用 Vim 插件管理插件。我没数过,但是三四个实现肯定是有的。我不喜欢它们的原因,一是迁移成本,二是依赖太巨大:我在一个新环境上配置 Vim 时,我不希望必须访问 GitHub,而且是几十个 clone,这得多慢啊……而且这还意味着它需要有 git,能够联网,DNS 解析正常,等等。这些在各种奇怪的环境里都不容易实现啊。

后来,Vim 8 出来了,内建 packages 管理功能。不愧是官方出品,没有奇怪的依赖。插件来源用户随意,把文件放对位置就可以了。

然后,我又遇见了 git-subrepo,给了 submodule 以外的选择。简单来说它有点 vendoring 的感觉,把第三方仓库放自己里边了。如果目标没有安装 git-subrepo,也没有关系,所有文件都在这儿了,只是没有与第三方仓库同步的能力了而已。

而且,git-subrepo 和 git-notes 类似,它使用额外的 ref 来存储第三方仓库的数据。但与 notes 不同的是,subrepo 已经存在于第三方仓库了,所以我不必把 subrepo 的 ref 推得到处都是,还在每个地方 fetch 下来浪费空间。万一第三方仓库没了或者搬家了,关系也不大,反正我用的代码还在我的仓库里呀。

git-subrepo 是很棒的「pay as you go」设计呢。

有了这两个东西,我就可以尝试一下新的 Vim 插件管理方案了。刚好我需要安装 vim-go,所以就开始尝试啦~

于是开始安装:

cd ~/.vim
git subrepo clone git@github.com:fatih/vim-go.git pack/go/start/vim-go

Vim 8 会自动加载pack/*/start/*下的东西(它下边的目录跟原来的 ~/.vim 布局和用法一样),:packadd命令会加载pack/*/opt/*下的东西。

然后记得设置环境变量。首先是 GOPATH,这是放所有 Go 的源码和编译出来的库和二进制文件的地方。Go 1.8 开始默认 ~/go,但是也需要设置 GOPATH,不然 vim-go 不认。然后得把 $GOPATH/bin 加到 PATH 里去。

确认环境变量设置好之后,打开一个新的 Vim,执行:GoInstallBinaries命令安装依赖。如果中途有神秘失败的,可以把地址拷出来,在命令行里手动调用 go install xxx 安装。部分软件需要访问 Google 的服务器,国内用户注意自行配置一下国际互联网的访问。

安装好之后就有补全、自动格式化、自动加 imports、重命名什么的啦。具体看 vim-go 的文档吧。不过有几点我稍微调整了一下。

我使用 syntastic 来进行代码检查。它默认不启用针对 Go 的检查,需要配置一下:

let g:syntastic_go_checkers = ['go', 'golint']

syntastic 的显示效果比 :GoMetaLinter 命令使用 quickfix 窗口的要好很多哦。

另外不要使用 :GoLint 这个命令,它连非常简单的未定义变量都检查不出来。

而且 vim-go 好 chatty,连跳转到定义都要用显眼的颜色来报告一下成功了……安静真的很珍贵的说。

:GoDoc 我还不会用,因为它报错了:

vim-go: gogetdoc: couldn't get package for .../t.go: can't find package containing .../t.go^@

写 Go 的时候,特别是调试的时候,加句fmt.Printf得记得修改 import,不需要了还得再去删掉才能通过编译……Go 编译是快了,但是处理未使用变量比等待编译结束更令人心烦啊。

有了 goimports 这个命令之后,不需要在开发过程中反复手动修改导入的包列表了。它可以自动添加和删除 import。不过 vim-go 并没有提供在保存时自动调用的支持,所以我就自己动手了。

首先禁用掉它自己的自动格式化:

let g:go_fmt_autosave = 0

然后,是我自己设置的自动命令:

if !exists('#GoImports') && executable("goimports")
  augroup GoImports
    au!
    autocmd BufWrite *.go silent call s:GoImports()
  augroup END
endif

function! s:GoImports()
  let pos = getpos('.')
  let na = line('$')
  %!goimports
  if v:shell_error
    undo
  endif
  let nb = line('$')
  let pos[1] = pos[1] + nb - na
  call setpos('.', pos)
endfunction

还有待提高(比如 undo 的处理),但至少能用。

Category: Vim | Tags: vim go Git
5
11
2015
28

为什么我反对普遍地静态链接?

这是我查看知乎私信时不小心瞅到的问题所触发的。由于 Go 在国内的兴起,我对这个问题也多有思考,就放在这里记录一下好了。知乎的链接我就不贴了,带 nofollow 的都懒得贴了。

首先,我们搞清楚问题是什么。或者说,我反对的究竟是什么?

静态链接,即早期唯一的一种链接出二进制可执行文件的方式,把所有程序需要用到的库全部打包到一个文件里边。后来,由于存储空间越来越不够用,所以发展出了动态共享库,也就是把库编译成 so、dll 或者 dylib 这种由动态库装载器在程序运行前或者运行时进行链接的方案。

静态链接的优点

  1. 方便分发,不会因为库的升级而导致程序无法运行。这一点没有严格指定依赖版本的 Arch Linux 用户应该都有所体会。当你更新某个库(比如 boost 或者 icu 什么的)之后,动态链接到旧库的程序会出错。

  2. 效率稍高。这个反正人类是体会不到的。

基于第一点,用于急救的重要程序最好使用静态链接,特别是 busybox。以前我会安装 busybox 的,后来因为 Arch 改用动态链接 C 库了,对于我不再有意义,所以卸载了。以后 C 库如果出问题就直接重启进救援系统了。

另外我还有静态链接的 32 位 Vim,为的是在 Vim 依赖库更新而 Vim 没更新时依旧有个顺手的编辑器可用。我一直自行编译 Vim 因此这个曾经十分有用。不过由于现在对经常变动导致问题的解释器支持采用运行时动态链接,所以基本不受影响了。

静态链接的缺点

这个可以列出长长的一串了。

  1. 占用磁盘空间。我就不怎么喜欢 Haskell 写的程序,太占硬盘了,一个程序就几十M。当然换新电脑之后目前硬盘空间有富余。但是它们还是会渐渐被我的各种源码和虚拟机什么的填满的。

  2. 占用内存空间。可执行文件在执行时是需要映射到内存中的。如果使用动态链接,那么因为是同一文件,所以在内存时只需要映射一份就可以了。而静态链接,不仅因为来源于不同的文件而需要加载、映射多次,而且因为来自于不同的构建等原因,逻辑上相同的代码往往并不会造成映射之后的内存页相同,使得内存去重机制(如 UKSM)失效。

    别说内存是白菜价,除非你来给我手上的笔记本、VPS、服务器、路由器、单板机等都配置个几十G的内存,我付给你等质量的白菜。还记得比尔·盖茨说过的话吗——「640K足够了」。够了吗?

  3. 占用 I/O 带宽。可执行文件越大,在内存里没有缓存时需要从外存读取的数据也就越多,耗时也就越长。而因为文件体积增大,内存资源越发不够用,I/O 缓存越少,导致缓存命中更低。

  4. 占用网络带宽。你可执行文件是从网上下的吧?你在国内看个视频还挺流畅,但是到世界各地去下软件你试试看?

  5. 运行时链接。我写了一个程序,支持 MySQL、SQLite3、PostgreSQL、MongoDB、Oracle 等等等等数据库。但是你显然不会用到所有的数据库支持吧?那你为什么要所有这些数据库的连接库来用一个 SQLite3 数据库的功能呢?使用运行时链接(dlopen 那些函数),程序可以在运行的时候动态判断并加载它此次运行所需要的动态库。

  6. 升级。openssl 爆出了一个很严重的安全漏洞,已经被修复了,你怎么办?当然是升级呗。那你希望是更新一个几M的包然后重启服务器解决问题,还是下载好几百M的程序、更新每一个你所用到的使用了 openssl 的程序?更何况那些程序本身不一定都更新了,也许为了安全你得自行编译其中的很大一部分(你可以期望有一个安全团队在半夜爬起床去更新一个软件,但是你觉得上千项目的开发者都会这么做吗)。你也不一定能够找到所有静态链接了有漏洞的 openssl 版本的程序,万一漏掉一个,你整个服务器的安全性就没了(所以 openssl heartbleed 漏洞更新之后建议是重启系统而不是重启各服务)。

更别说更底层的库了,比如 C 库或者 C++ 库。不至于人家更新了一行代码,你就要重装整个系统吧?

静态链接有它自身的用处,但是它并不适合所有情况,甚至并不适合大多数情况。动态链接以其微小的运行效率损失为代价,为不论是最终用户还是开发者、打包者提供了更为优秀的库管理方案。之所以很多人看到静态链接相对于动态链接的优势,我认为还是因为他们没什么机会看到静态链接、尤其是大量静态链接会带来的问题。

你不需要把程序都静态链接。你需要的只不过是一个优秀的包管理器和维护团队而已

Category: Linux | Tags: linux 编译 go 编程语言
1
6
2015
41

众编程语言间的 swapview 之战

swapview 起源于我很早之前看到的一个 shell 脚本。当时正在学习 Haskell,所以就拿 Haskell 给实现了一遍。为了对比,又拿 Python 给实现了一遍。而如今,我又在学习另一门新的语言——Rust,也拿 swapview 来练习了。相比仅仅输出字符串的「Hello World」程序,swapview 无疑更实际一些:

  • 文件系统操作:包括列目录、读取文件内容
  • 数据解析:包括简单的字符串处理和解析,还有格式化输出
  • 数据处理:求和啊排序什么的
  • 流程控制:循环啊判断啊分支什么的都有
  • 错误处理:要忽略文件读取错误的

因此,swapview 成为了依云版的「Hello World」:-)

感谢所有给 swapview 提交代码的朋友们

本文只是借 swapview 这个程序,一窥众编程语言的某些特征。很显然,编程语言们各有所长,在不同的任务下会有不同的表现。而且 swapview 各个版本出自不同的人之手,代码质量也会有所差异。

闪耀!那些令人眼前一亮的语言们

从运行效率上来看,C 如预期的一样是最快的。但令人惊讶的是,由我这个 Rust 初学者写的 Rust 程序竟然紧随其后,超越了 C++。

而原以为会跟在 Rust 之后的 C++,却输给了作为脚本语言存在的 Lua 语言的高效实现 LuaJIT(与 Rust 版本相当)。而且非 JIT 版本的 Lua 5.1 和 5.2 也都挺快的。Lua 这语言自带的功能非常少,语法也简单,但是效率确实高,让人又爱又恨的。

失望!那些没预期中的高效的语言们

没想到 Python 2 也挺快的,很接近 Go 了。PyPy 大概是因为启动比较慢的原因而排在了后面。Python 3 有使用两个版本的代码,Python3_bytes 把文件读取改为使用 bytes,仅在需要的时候才解码成 str。仅此之差,运行速度快了10%。可见 Python 的 Unicode 处理十分耗时,难怪 Python 3 在各种测试中都比 Python 2 要慢上一截。至于 PyPy3,怎么跑到那么靠后的地方去了呢……

Go 很快。至少比 Python 快。但也仅此而已了,不仅比 C++ 慢,甚至连 Lua(非 JIT 版)都不如。Go 语言版本虽然不是我写的,但我看过代码,感觉很原始。至少比 Lua 原始。看起来 Go 只不过是带接口和并发支持的 C 而已。而且,作为静态类型的编译型语言,却我却有一种很不放心的感觉。大约是因为我改动时发现传给 fmt.Printf 的参数类型和数目错了都不会得到警告或者错误的原因。而且我从来没见过 Go 编译时出现警告,对于还没入门的初学者写的、改过的程序,这样子不科学啊。早期我倒是见过 Go 报错了,但那只不过是编译器还不完善的表现而已。

传闻 NodeJS 很快。但至少它在 swapview 这种脚本中没能体现出来。正常版本比 Python 3 还要慢一点。而使用异步啊并行什么的版本还要慢上差不多三分之一,不知道怎么搞的。

编译型的 Chicken、OCaml、Haskell 都排在了一众脚本语言后边,虽然很可能是对语言本身不熟导致写出来的程序比较慢,但还是挺令人失望的。经过高手优化的 Haskell2 版本效率接近于 Python 3,但也到此为止了(因为不想使用 cabal 安装依赖,所以 Haskell2 没有参与这场对决)。我曾见过有人把 Haskell 代码优化到比 C 还快,但我宁愿去看汇编也不要去读那种代码……

Lisp 系(Chicken、Racket、SBCL(标注为 CommonLisp 的项)、Guile)也都挺慢的。不知道 LispWorks 之类的会不会快一大截呢。

预料之中的以及结果截图

Ruby 比 Python 略慢一点。

Java、Elixir 比较靠后。没办法,它们启动慢。也许以后我会出不考虑启动时间的版本。

以下是本文发表前的测试结果截图。其中 Erlang 版本因为有问题被信号所杀所以被扔在了最后。

测试结果截图

测试使用的是benchmark子目录中的 Rust 程序,使用cargo build --release命令即可构建。另外也可以使用 farseerfc 的 Python 脚本。

代码量

Elixir 代码量挺少的。Python、Ruby 也挺不错。Java 版本竟然跟 Haskell 一样。不管是 JavaScript 还是 CoffeeScript 都比较长,比 Java 还长。Rust 比 Python 长不少,但也比 Go 短不少。而 Go 比起 C、C++ 要短一些。最长的,除了我不了解的 Pascal,竟然还有因为程序出错还没有测试的 Erlang!如果不算按行读取的 line_server.erl 的放大,只有不到一百行,倒还不算多。

                  Elixir:   50
                   Julia:   51
           Python3_bytes:   53
                  Python:   56
                    Ruby:   56
                  Racket:   58
                    Bash:   63
                   OCaml:   65
          CommonLisp_old:   67
          CommonLisp_opt:   67
           Bash_parallel:   69
             C++14_boost:   69
                   Guile:   70
                 Haskell:   73
                 Chicken:   75
                    Java:   75
                  NodeJS:   76
                    Vala:   78
                Haskell2:   81
                       D:   86
                    Rust:   88
                   C++14:   89
                  CSharp:   91
                     Lua:   91
            NodeJS_async:   93
            CoffeeScript:   93
   CoffeeScript_parallel:   95
                     PHP:   97
           Rust_parallel:   98
                      Go:  103
                   C++11:  128
                   C++98:  141
                       C:  149
              FreePascal:  185
                  Erlang:  232

编译速度

这个比较非常粗糙,比如联网下载依赖也被算进去了。不过可以肯定,不算下载依赖部分的话,Rust 是最慢的!其次是 Haskell。标榜编译速度非常快的 Go 并不是最快的,和 C++ 不相上下(当然不知道代码复杂之后会如何了)。

0.36 C
0.60 FreePascal
0.80 OCaml
0.83 CoffeeScript_parallel
1.48 CSharp
1.67 Vala
1.68 Erlang
2.13 NodeJS_async
2.27 C++14
2.49 Go
2.53 CoffeeScript
2.90 C++11
3.01 C++98
3.23 Java
3.52 Racket
3.98 NodeJS
6.05 CommonLisp_opt
7.07 D
9.01 C++14_boost
10.41 Haskell
13.07 Rust
14.74 Chicken
15.37 Rust_parallel

结语

这个项目最初只是练习而已。后来不同语言的版本有点多,于是才演变成众编程语言的竞技。也就随意地测试了一下在给定需求下不同语言的表现而已。其实比较有意思的部分,一是使用正在学习的编程语言写作程序的新奇感、新知、新的领悟(这也是我的测试程序使用 Rust 编写的原因),二是对比不同编程语言的风格和对同样需求的处理方式。

各位读者对 swapview 有任何补充和改进,欢迎贡献代码哦~项目地址:https://github.com/lilydjwg/swapview

更新区

2015年1月9日更新:又收到了不少版本和改进,以下是最新的测试结果。很不幸地,现在已经跑得很快的 Erlang 在测试中又没反应被杀掉了。并行版的 Rust 的结果很不稳定,这次跑得好快!C++ 的除了 C++98 版的之外都到 Rust 前边去了。PHP 竟然比 LuaJIT 还要快!D 怎么到 PyPy 后边去了。

2015年1月9日的测试结果截图

2015年1月10日更新:C++ 版本继续改进,好多都超越 C 了,Rust 1.0.0alpha 的并列版本又快又稳定,Erlang 版本终于跑完了全部测试而没有出事,LLVM 版 D 快了好多。

2015年1月10日的测试结果截图

2015年1月18日更新:继续更新。又添加了若干语言,不过期待中的 Nim、Zimbu 以及传统脚本语言 Perl、Tcl 依旧缺席中。另外,正文也进行了更新,重新计算了代码量,添加了编译速度的粗略比较。

2015年1月18日的测试结果截图

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com