みーのぺーじ

みーが趣味でやっているPCやソフトウェアについて.Python, Javascript, Processing, Unityなど.

ループの速度を Python, Rust, Node.js で比較する

ループ(繰り返し処理)の書き方はプログラミング言語により様々です.使いやすい書き方を選択すればよいとは思いますが,気になったので処理速度を比較してみました.

みーがよく使っている,Python, Rust, Node.js について,1千万個の要素を含む配列を用意して,前から順番に要素を取得する処理速度を計測しました.

なお,言語によって配列という仕組みが異なるので,この記事では,ヒープ領域に保持されるものを配列と呼ぶことにします.したがって Python では List を,Rust では Vec を,Node.js では Array を用います.

環境

すべて共通の環境で実行して比較しました.

$  lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 11 (bullseye)
Release:        11
Codename:       bullseye

$ lscpu
Architecture:  x86_64
Model name:  12th Gen Intel(R) Core(TM) i5-12400

$ python --version
Python 3.11.6

$ node --version
v18.18.2

$ rustc --version
rustc 1.75.0-nightly (0f44eb32f 2023-11-09)

Python

from benchmarker import Benchmarker

with Benchmarker(loop=1,cycle=1) as bench:
    n = 10 * 1000 * 1000
    s = list(range(0, n))

    @bench(None)
    def _(bm):
        for _ in bm:
            pass

    @bench("index-for")
    def _(bm):
        for _ in bm:
            for i in range(n):
                s[i]
                
    @bench("index-while")
    def _(bm):
        for _ in bm:
            i=0
            while i<n:
                s[i]
                i+=1

    @bench("iter")
    def _(bm):
        for _ in bm:
            for j in s:
                pass

結果

$ python run.py 
## benchmarker:         release 4.0.1 (for python)
## python version:      3.11.6
## python compiler:     GCC 10.2.1 20210110
## python platform:     Linux-5.15.0-88-generic-x86_64-with-glibc2.31
## python executable:   /usr/local/python/current/bin/python
## cpu model:           12th Gen Intel(R) Core(TM) i5-12400  # 2500.000 MHz
## parameters:          loop=1, cycle=1, extra=0

##                                       real    (total    = user    + sys)
(Empty)                                0.0000    0.0000    0.0000    0.0000
index-for                              0.1266    0.1300    0.1300    0.0000
index-while                            0.3668    0.3600    0.3600    0.0000
iter                                   0.0441    0.0500    0.0500    0.0000

Python ではリストのインデックスで取得するよりも,イテレータを利用した方が速いことが分かりました.また,for i in range(n) よりも while i<n を使用した方が3倍遅いことが分かりました.

Rust

#![feature(test)]
use std::hint::black_box;
extern crate test;
static N: usize = 10_000_000;

#[bench]
fn empty(b: &mut test::Bencher) {
    b.iter(|| black_box(1))
}

#[bench]
fn bench_index(b: &mut test::Bencher) {
    let s = (0..N).collect::<Vec<usize>>();
    b.iter(|| {
        let mut i: usize = 0;
        while i < N {
            black_box(s[i]);
            i += 1;
        }
    });
}

#[bench]
fn bench_iter(b: &mut test::Bencher) {
    let s = (0..N).collect::<Vec<usize>>();
    b.iter(|| {
        for j in &s {
            black_box(j);
        }
    });
}

std::hint::black_box はベンチマークを実行するよう指示する,コンパイラへのヒントです*1

結果

$ cargo bench
   Compiling rust v0.1.0 (/workspaces/loop-bench/rust)
    Finished bench [optimized] target(s) in 0.16s
     Running unittests src/lib.rs (target/release/deps/rust-8433621fac8b73e1)

running 3 tests
test bench_index ... bench:   3,617,616 ns/iter (+/- 135,049)
test bench_iter  ... bench:   2,283,295 ns/iter (+/- 38,180)
test empty       ... bench:           0 ns/iter (+/- 0)

test result: ok. 0 passed; 0 failed; 0 ignored; 3 measured; 0 filtered out; finished in 6.11s

Rust は Python と同様の結果でした.

Node.js

const { suite, add, cycle, complete, save } = require('benny');
const n = 10 * 1000 * 1000;
const s = Array.from(Array(n), (_, k) => k);

suite(
    "bench",
    add("for", () => {
        for (let i = 0; i < s.length; i++) {
            s[i];
        }
    }),
    add("for2", () => {
        for (let i = 0, n = s.length; i < n; i++) {
            s[i];
        }
    }),
    add("for-each", () => {
        s.forEach((_) => { });
    }),
    add("map", () => {
        s.map((_) => { });
    }),
    add("iter", () => {
        for (const _ of s) { }
    }),
    cycle(),
    complete(),
    save({ file: 'run', version: '1.0.0' })
);

結果

$ node run.js
Running "bench" suite...
Progress: 100%

  for:
    431 ops/s, ±0.18%   | 0.23% slower

  for2:
    432 ops/s, ±0.23%   | fastest

  for-each:
    25 ops/s, ±0.50%    | 94.21% slower

  map:
    14 ops/s, ±0.51%    | slowest, 96.76% slower

  iter:
    283 ops/s, ±0.58%   | 34.49% slower

Node.js では for でインデックスを用いて配列を取得するのが最も速いようです.forEach関数やmap関数は比較的遅いです.for of を用いてイテレータを使用すると少し遅くなるようです.

配列の長さを変数に代入してキャッシュすることで高速になるような気がしますが,ほとんど差はないです*2Array: length の取得は繰り返し処理に比べると十分高速みたいです*3

Node.js では Rust や Python とは異なる結果が得られました.JavaScript では伝統的な for (let i = 0; i < s.length; i++) {} が好まれている印象がありますが,速度的な問題も考慮されているのかもしれません*4

まとめ

言語により配列から要素を取得する処理速度と書き方は異なる傾向になることが分かり,実際に測定してみて勉強になりました.