nim / rust / pony の速度比較
※ 最初に申し上げておきますが、とても浅はかな比較です。
Rust は言わずもがな、「最も愛されているプログラミング言語」4年連続1位1の言語です。
データの生存期間を型で管理することで、GCなしにメモリセーフなプログラミングを可能にしてますね。
トレイトやマクロなどもあり、表現力も豊富で開発も盛んなので注目ですね。
実行速度もちょくちょく C++ に勝ったり負けたりと、非常に高速なことで知られます。
Nim は柔軟な書き方ができる言語で、最近お気に入りです。
ついこの間、めでたく v1.0 を迎えて安定期に入りました🎉
遅延リストのリテラルがないのが残念です。マクロはあるのでその気になれば自力実装はできそうですが、現時点では私には Nim 力が足らず無理です。
Nim で書かれたコードは一度 C のコードに変換されてから2、C のコンパイラでさらにネイティブコードにコンパイルされます。
Pony は昨日3初めて知った言語ですが、割と前からあるみたいですね。
言語構文レベルでアクターモデルを採用しているのが、たぶん最大の特徴です。
Rust に似て変数の所有権のようなもの4を型に持たせることで、アクターモデルでありながらデータのコピーを極力減らすことができ、高速かつ安全なプログラミングを可能にしているようです。
補完にまで対応した IDE やエディタ拡張が見当らないので、それがネックですね…。
まだセルフホストには至っていない模様。
環境
$ cat /etc/os-release NAME="Ubuntu" VERSION="18.04.3 LTS (Bionic Beaver)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 18.04.3 LTS" VERSION_ID="18.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=bionic UBUNTU_CODENAME=bionic
$ rustc --version rustc 1.39.0 (4560ea788 2019-11-04)
$ nim --version Nim Compiler Version 1.0.2 [Linux: amd64] Compiled at 2019-10-22 Copyright (c) 2006-2019 by Andreas Rumpf git hash: 193b3c66bbeffafaebff166d24b9866f1eaaac0e active boot switches: -d:release
$ ponyc --version 0.33.0-98c36095 [release] compiled with: llvm 7.0.1 -- cc (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0 Defaults: pic=true
比較
なるべく公平になるようにコードを書いているつもりです。
FizzBuzz
コード
Rust
fn fizz_buzz(n: i32) { let s = match (n % 3, n % 5) { (0, 0) => "FizzBuzz".to_string(), (0, _) => "Fizz".to_string(), (_, 0) => "Buzz".to_string(), _ => n.to_string(), }; println!("{}", s); } fn main() { for x in 1..=100 { fizz_buzz(x); } }
Nim
proc fizzBuzz(n: int) = let (x, y) = (n mod 3, n mod 5) let s = if (x, y) == (0, 0): "FizzBuzz" elif x == 0: "Fizz" elif y == 0: "Buzz" else: $n echo s for x in 1..100: fizzBuzz x
Pony
use "collections" actor FizzBuzz let _env: Env new create(env': Env) => _env = env' be fizz_buzz(x: I32) => let s = match (x % 3, x % 5) | (0, 0) => "FizzBuzz" | (0, _) => "Fizz" | (_, 0) => "Buzz" else x.string() end _env.out.print(s) actor Main new create(env: Env) => let fb = FizzBuzz(env) for x in Range[I32](1, 101) do fb.fizz_buzz(x) end
コンパイル速度
Rust
$ time -p rustc -Copt-level=3 -Clto -Cpanic=abort fizzbuzz.rs real 2.23 user 1.96 sys 0.39
Nim
$ time -p nim c -d:release --opt:speed --hints:off fizzbuzz CC: stdlib_io.nim CC: stdlib_system.nim CC: fizzbuzz.nim real 2.03 user 1.70 sys 0.48
Pony
$ time -p ponyc --verbose=0 real 2.84 user 2.03 sys 0.81
Rust | Nim | Pony | |
---|---|---|---|
real | 2.23 | 2.03 | 2.84 |
user | 1.96 | 1.64 | 2.12 |
sys | 0.39 | 0.54 | 0.70 |
コンパイル速度はそこまで差が出ないようです。
※ ただし、Nim はコンパイル結果をキャッシュするので、同じプロジェクトであれば2度目以降のコンパイルが非常に高速になります。 --forceBuild
/ -f
オプションを付けるかキャッシュを削除すると1度目と同じ条件になります。今回は毎回キャッシュを削除しました。
※ Rust は最適化オプションをいくつか外すことで、成果物のファイルサイズを犠牲(3倍近く)にかなり高速(4分の1以下)になりました。
ファイルサイズ
Rust
$ ls -l fizzbuzz -rwxrwxrwx 1 htsign htsign 928400 11月 21 21:58 fizzbuzz*
Nim
$ ls -l fizzbuzz -rwxrwxrwx 1 htsign htsign 88864 11月 21 21:59 fizzbuzz*
Pony
$ ls -l fizzbuzz -rwxrwxrwx 1 htsign htsign 163176 11月 21 21:59 fizzbuzz*
Rust | Nim | Pony |
---|---|---|
928,400 bytes (907KB) | 88,864 bytes (87KB) | 163,176 bytes (159KB) |
※ KB 表記の端数は四捨五入
Rust が圧倒的にデカいですね。
Rust のバイナリがデカい理由はデバッグシンボルが残っているからかと思われます。最適化オプション有効にしてるのに…。
ここではコマンド単体で完結することを前提とするのでこれを最終結果とします。
実行速度
Rust
$ time -p ./fizzbuzz > /dev/null real 0.01 user 0.00 sys 0.01
Nim
$ time -p ./fizzbuzz > /dev/null real 0.00 user 0.00 sys 0.00
Pony
$ time -p ./fizzbuzz > /dev/null real 0.02 user 0.00 sys 0.01
Rust | Nim | Pony | |
---|---|---|---|
real | 0.01 | 0.00 | 0.02 |
user | 0.00 | 0.00 | 0.00 |
sys | 0.01 | 0.00 | 0.01 |
まぁ当然ですが、負荷が軽すぎてほとんど差が出ないですね…。
45番目のフィボナッチ数
※ 45番目なのは計算時間的にちょうどよさそうなので。
コード
Rust
fn fibonacci(n: i64) -> i64 { match n { 1 | 2 => 1, _ => fibonacci(n - 2) + fibonacci(n - 1), } } fn main() { println!("{}", fibonacci(45)) }
Nim
func fibonacci(n: int64): int64 = case n of 1, 2: 1.int64 else: fibonacci(n - 2) + fibonacci(n - 1) echo fibonacci(45)
Pony
primitive Fibonacci fun fib(n: I64): I64 => match n | 1 | 2 => 1 else fib(n - 2) + fib(n - 1) end actor Main new create(env: Env) => env.out.print(Fibonacci.fib(45).string())
コンパイル速度
※ 実行コマンドは FizzBuzz のとほとんど変わらないので割愛
Rust | Nim | Pony | |
---|---|---|---|
real | 2.20 | 2.00 | 1.19 |
user | 1.75 | 1.64 | 0.89 |
sys | 0.45 | 0.48 | 0.29 |
なぜか Pony が FizzBuzz の場合と比べてちょっと速くなりました。
ファイルサイズ
Rust | Nim | Pony |
---|---|---|
924,024 bytes (902KB) | 84,288 bytes (82KB) | 158,696 bytes (155KB) |
こちらも相変わらず Rust がダントツにデカいです。
実行速度
Rust | Nim | Pony | |
---|---|---|---|
real | 2.88 | 3.45 | 3.52 |
user | 2.87 | 3.45 | 3.48 |
sys | 0.01 | 0.00 | 0.03 |
さっきと比べて少し差が出ましたね。
Rust、やはり速い。
並列性を上げて計算すれば Pony も速くなったりするのでしょうか。
以上、Pony はまだまだ発展途上ですし、Rust もバージョン上がるごとに最適化が進んだりするので、本当にガバガバ比較です。
結局のところ好みで選べばいいんじゃないかと思います。
並列計算が目的なら Erlang や Elixir などの選択肢もあります。が、あれはプロセス単位での並列なのでちょっと事情が違うかも。
-
https://insights.stackoverflow.com/survey/2019#technology-_-most-loved-dreaded-and-wanted-languages↩
-
いわゆるトランスパイル↩
-
投稿時間が日を跨いでしまったので…。↩
-
Pony では Reference Capabilities と呼ばれます。↩