ループ(繰り返し処理)の書き方はプログラミング言語により様々です.使いやすい書き方を選択すればよいとは思いますが,気になったので処理速度を比較してみました.
みーがよく使っている,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
を用いてイテレータを使用すると少し遅くなるようです.
配列の長さを変数に代入してキャッシュすることで高速になるような気がしますが,ほとんど差はないです*2.Array: length
の取得は繰り返し処理に比べると十分高速みたいです*3.
Node.js では Rust や Python とは異なる結果が得られました.JavaScript では伝統的な for (let i = 0; i < s.length; i++) {}
が好まれている印象がありますが,速度的な問題も考慮されているのかもしれません*4.
まとめ
言語により配列から要素を取得する処理速度と書き方は異なる傾向になることが分かり,実際に測定してみて勉強になりました.