RustとPythonをブートストラップ法で速度を比較してみる

こんにちは!FLINTERS BASEの梶山です。

今回はRustというプログラミング言語について紹介したいと思います。主にRustの魅力としてはメモリ安全性や速度、開発者体験が高いことが挙げられます。 自分はデータエンジニアをしており普段はPythonを使用しているのですがPolars,Pydantic等のライブラリのコア部分で使われていたりもします。

ところでなぜデータエンジニアなのにRustを勉強しているのかというと個人的に書いていて楽しいからです。 伝わらないかも知れませんがマニュアル運転でシフトガチャガチャしている時?みたいな感じです。
あとRustでPythonのライブラリを作成することができるので、どうしてもPythonだと遅い時とかはRustでコードを書いて解決出来たりする時が来るかもとか思っていたりもして勉強しています。

※Rustがもっとどんな感じのプログラミング言語なのか知りたい方は以下のリンク先のドキュメントやYoutubeなどに上がっている動画を閲覧することをオススメします。 doc.rust-jp.rs

※全然読めてないですが以下の本もオススメです。上のリンクをやってから読むことをオススメします。 gihyo.jp

PythonとRustで実行速度を比較

Pythonは実行速度が遅いスクリプト言語ですが実際どれくらいRustと実行速度の差があるのでしょうか。 今回はとりあえずブートストラップ法を実装して簡単にPythonとRustの速度を比較してみました。
※Rust初心者なのでもっといい感じのコードの書き方あったら教えてください🙇

import random
import statistics
import time

n_data = 10_000
n_bootstrap = 1_000

data = [random.random() for _ in range(n_data)]

means = []
medians = []
variances = []

start = time.time()

for _ in range(n_bootstrap):
    sample = random.choices(data, k=n_data)
    means.append(statistics.mean(sample))
    medians.append(statistics.median(sample))
    variances.append(statistics.variance(sample))

end = time.time()

print(f"mean:{statistics.mean(means):.5f}")
print(f"median:{statistics.mean(medians):.5f}")
print(f"variance:{statistics.mean(variances):.5f}")
print(f"time:{end - start:.3f} sec")

# mean:0.50190
# median:0.49925
# variance:0.08242
# time:17.719 sec
use statrs::statistics::Statistics;
use rand::rng;
use rand::prelude::IndexRandom;
use std::time::Instant;

fn main() {
    let n_data = 10_000;
    let n_bootstrap = 1_000;
    let mut rng = rng();

    let data: Vec<f64> = (0..n_data).map(|_| rand::random::<f64>()).collect();

    let mut means = Vec::with_capacity(n_bootstrap);
    let mut variances = Vec::with_capacity(n_bootstrap);
    let mut medians = Vec::with_capacity(n_bootstrap);

    let start = Instant::now();

    for _ in 0..n_bootstrap {
        let mut sample: Vec<f64> = (0..n_data)
            .map(|_| *data.choose(&mut rng).unwrap())
            .collect();
        
        let mean = (&sample).mean();
        let variance = (&sample).variance();
       
        sample_clone.sort_by(|a, b| a.partial_cmp(b).unwrap());
        let median = (sample[sample.len() / 2 - 1] + sample[sample.len() / 2] ) / 2.0;

        means.push(mean);
        variances.push(mean);
        medians.push(mean);
    }

    let duration = start.elapsed();

    let mean_mean = means.iter().sum::<f64>() / means.len() as f64;
    let mean_variance = variances.iter().sum::<f64>() / variances.len() as f64;
    let mean_median = medians.iter().sum::<f64>() / medians.len() as f64;

    println!("mean:{:.5}", mean_mean);
    println!("variance:{:.5}", mean_variance);
    println!("median:{:.5}", mean_median);
    println!("time:{:.3} sec", duration.as_secs_f64());

    // mean:0.49228
    // variance:0.08381
    // median:0.48919
    // time:10.567 sec
}

Rustの方が約7秒ぐらい早いですね。自分みたいなRust初心者が書いたコードでもこんなに速くなるのは驚きました。 最後にPythonでnumpyを使うとどれぐらい速くなるのか見てみましょう。

import random
import numpy as np
import time

n_data = 10_000
n_bootstrap = 1_000

data = [random.random() for _ in range(n_data)]

means = []
medians = []
variances = []

start = time.time()

for _ in range(n_bootstrap):
    sample = np.random.choice(data, replace=True, size=n_data)
    means.append(np.mean(sample))
    medians.append(np.median(sample))
    variances.append(np.var(sample))

end = time.time()

print(f"mean:{np.mean(means):.5f}")
print(f"median:{np.mean(medians):.5f}")
print(f"variance:{np.mean(variances):.5f}")
print(f"time:{end - start:.3f} sec")

# mean:0.49995
# median:0.49884
# variance:0.08345
# time:1.074 sec

numpyが圧倒的です。 実際Pythonは有名な計算ライブラリとかは大体C/C++,Fortranなどで書かれてたりするのでかなり速いです。 あとやっぱりPythonだとライブラリが充実しているのもあって書いててラクですね。

終わりに

これからも趣味でのんびりRustの勉強はしていきたいなと思いますが、数値計算ではPythonを使っておとなしく巨人の肩に乗るのがいいですね。