htsign's blog

ノンジャンルで書くつもりだけど技術系が多いんじゃないかと思います

nim / rust / pony の速度比較

※ 最初に申し上げておきますが、とても浅はかな比較です。

Rust は言わずもがな、「最も愛されているプログラミング言語」4年連続1位1の言語です。
データの生存期間を型で管理することで、GCなしにメモリセーフなプログラミングを可能にしてますね。
トレイトやマクロなどもあり、表現力も豊富で開発も盛んなので注目ですね。
実行速度もちょくちょく C++ に勝ったり負けたりと、非常に高速なことで知られます。

Nim は柔軟な書き方ができる言語で、最近お気に入りです。
ついこの間、めでたく v1.0 を迎えて安定期に入りました🎉
遅延リストのリテラルがないのが残念です。マクロはあるのでその気になれば自力実装はできそうですが、現時点では私には Nim 力が足らず無理です。
Nim で書かれたコードは一度 C のコードに変換されてから2、C のコンパイラでさらにネイティブコードにコンパイルされます。

Pony は昨日3初めて知った言語ですが、割と前からあるみたいですね。
言語構文レベルでアクターモデルを採用しているのが、たぶん最大の特徴です。
Rust に似て変数の所有権のようなもの4を型に持たせることで、アクターモデルでありながらデータのコピーを極力減らすことができ、高速かつ安全なプログラミングを可能にしているようです。
補完にまで対応した IDE やエディタ拡張が見当らないので、それがネックですね…。
まだセルフホストには至っていない模様。

環境

WSL Ubuntu on Windows 10

$ 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 などの選択肢もあります。が、あれはプロセス単位での並列なのでちょっと事情が違うかも。


  1. https://insights.stackoverflow.com/survey/2019#technology-_-most-loved-dreaded-and-wanted-languages

  2. いわゆるトランスパイル

  3. 投稿時間が日を跨いでしまったので…。

  4. Pony では Reference Capabilities と呼ばれます。