pyhaya’s diary

機械学習系の記事をメインで書きます

(速度比較) Rust でテキストファイルからデータを読み出す

Rust を使って、テキストファイルを開いて中のデータを読み出すということをやってみます。Rust では読み込み方法がいくつもあるのでそれの比較と、普段私が使っている Pythonnumpy.loadtxt との比較も行っていきます。

環境

  • Rust 1.39.0-nightly (nightly でなくても動く)
  • Windows 10 (Intel Corei7 10510U , RAM 16GB)
  • ( Python 3.7.3 )

読み込むデータ

時間を計測したいので、そこそこ大きなテキストファイルを用意します。ここは慣れている Python で行いました。

data = ""
num = 1000000
for i in range(num):
    data += f"{i} {i*2}"
    if i < num - 1:
        data += "\n"

with open("example.txt", "w") as f:
    f.write(data)

これを実行すると、タブで区切られた100万行2列のテキストデータが生成されます。

Rust でテキストファイルを読み込む

最初に、Rust を使ってこのテキストファイルを読み込んでx, y という名前の2つのベクトルにデータを入れるコードを書いてみます。

read_to_string を使う方法

まずは標準ライブラリに入っている関数の一つであるread_to_string を使って書いてみます。

use std::fs::read_to_string;

pub fn open_file(filename: &str) -> (Vec<f32>, Vec<f32>){
    let data = read_to_string(filename);
    let xy = match data {
        Ok(content) => content,
        Err(error) => {panic!("Could not open or find file: {}", error);}
    };

    let xy_pairs: Vec<&str> = xy.trim().split("\n").collect();
    let mut x: Vec<f32> = Vec::new();
    let mut y: Vec<f32> = Vec::new();

    for pair in xy_pairs {
        let p: Vec<&str> = pair.trim().split(" ").collect();

        x.push(p[0].parse().unwrap());
        y.push(p[1].parse().unwrap());
    }

    (x, y)
}

時間を計測してみると、

time cargo run --release

real    0m0.248s
user    0m0.156s
sys     0m0.047s

BufReader を使う方法

Rust には std::io にもファイル読み込みができる関数が用意されているのでこっちを使ってみます。

use std::fs::File;
use std::io::{BufReader, BufRead};

pub fn open_file(filename: &str) -> (Vec<f32>, Vec<f32>){
    let f = File::open(filename).unwrap();
    let buf = BufReader::new(f);
    let mut x: Vec<f32> = Vec::new();
    let mut y: Vec<f32> = Vec::new();

    for line in buf.lines() {
        let l: &str = &line.unwrap();
        let p: Vec<&str> = l.trim().split(" ").collect();

        x.push(p[0].parse().unwrap());
        y.push(p[1].parse().unwrap());
    }
    (x, y)
}

時間は

time cargo run --release

real    0m0.264s
user    0m0.188s
sys     0m0.047s

遅くなった(?)。

split_whitespace を使う方法

一番最初の方法では trim().split(" ") と、いちいち空白文字で分けるということをこっちで指定して分割を行っていました。しかし、Rust にはそれをやるための関数 split_whitespace が用意されているのでこっちを使ってみます。これは一番最初のコードでtrim().split(" ") の部分を split_whitespace() に書き換えるだけです。

時間を計測すると、

time cargo run --release

real    0m0.265s
user    0m0.188s
sys     0m0.063s

あんまり変わらない。

文字列から浮動小数点数への変換にlexical を使う

このコードをプロファイリングしてみるとわかるのですが、実行時間の40 % が 文字列処理で取られています。ファイルの読み込みとは少し話がずれますが、ここも別の方法を試してみます。&str から f32 への変換にここまでのコードでは parse() を使ってきました。しかし調べてみると、どうやらlexical というクレートが高速変換を謳っているということが分かりました。これを試してみます。

docs.rs

use lexical; をしたあとに次の部分を変更します。

// before
        x.push(p[0].parse().unwrap());
        y.push(p[1].parse().unwrap());

// after
        x.push(lexical::parse(p[0]).unwrap());
        y.push(lexical::parse(p[1]).unwrap());
time cargo run --release

real    0m0.789s
user    0m0.516s
sys     0m0.203s

今までで一番遅いですね。。。(なんで?)

(おまけ) numpy.loadtxt との比較

最後に、Numpy を使ったときの Python との比較をしてみます。

import numpy as np

x, y = np.loadtxt("example.txt").T

超簡単!! やはり Python で書くとシンプルですね。さて、時間は

real    0m13.730s 
user    0m12.297s
sys     0m1.438s

遅い。ちなみにimport numpy as npの部分は 0.39 secくらいなのでインポートに時間が食われているわけではない。さすがに Numpy でもテキスト読み込み処理に関してはそんなに早くないのか?

まとめ

  • read_to_string が一番速かった
  • Python より全然速い!
  • もっと速くする方法知りたい(&str -> f32 とか特に)

実践Rust入門[言語仕様から開発手法まで]

実践Rust入門[言語仕様から開発手法まで]