##どのくらい遅いか
フォルダ内のファイル名をVec
にまとめておきたい場面がありました。Rust で普通に実装すると下記のようになるでしょうか。ファイル数が少ないときは問題ないですが、私の環境では1万ファイル程度ある場合に 5~10sec も掛かってしまいました。
下記はダメな例です。(コメントで教えて頂きました)
use std::fs;
use std::io;
use std::path::Path;
fn read_dir<P: AsRef<Path>>(path: P) -> io::Result<Vec<String>> {
Ok(fs::read_dir(path)?
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| path.is_file())
.filter_map(|path| Some(path.file_name()?.to_str()?.into()))
.collect())
}
これ↓が良いコード
fn read_dir<P: AsRef<Path>>(path: P) -> io::Result<Vec<String>> {
Ok(fs::read_dir(path)?
.filter_map(|entry| {
let entry = entry.ok()?;
if entry.file_type().ok()?.is_file() {
Some(entry.file_name().to_string_lossy().into_owned())
} else {
None
}
})
.collect())
}
同じような処理を Node で実行すると 100msec 台で終了します。
JavaScript (Node) では↓のようになる。
const fs = require("fs");
function readdir(path) {
return fs.readdirSync(path, { withFileTypes: true })
.filter(e => e.isFile())
.map(e => e.name);
}
##なぜ遅い?
ソースを覗いてみましょう。
Rust: https://github.com/rust-lang/rust/blob/stable/src/libstd/sys/windows/fs.rs#L91
Node: https://github.com/nodejs/node/blob/v12.x/deps/uv/src/win/fs.c#L1588
Windows の場合、どちらもFindFirstFileW
、FindNextFileW
関数を使用しています。
Rust はReadDir
のイテレータを進めるたびにWIN32_FIND_DATA
のメモリ領域を確保してFindNextFileW
を呼び出しています。一方で Node はfs__readdir
関数の実行時に Array にすべてのファイル情報を格納しているようです。
なぜ Rust はこのような実装になっているのでしょうか?マルチスレッドを意識してるのかな?
遅い原因は以下の通り。私のコードが悪かったのでした。ちゃんちゃん。
fs::read_dirでせっかく得られた情報を捨てて、Path::is_fileとPath::file_nameを呼び出しています。Path::is_fileはファイルパスを使ってOSに問い合わせていますし、DirEntry::path+Path::file_nameはフルパスを生成してからファイル名だけを切り出すという処理をしています。
API を呼び出す実装
Windows
いつものようにwinapi
を使って書いていきます。エラー処理はanyhow
に丸投げです。
use anyhow::{anyhow, bail, Result};
use std::char::{decode_utf16, REPLACEMENT_CHARACTER};
use std::mem;
use std::path::Path;
use winapi::um::{
fileapi::{FindClose, FindFirstFileW, FindNextFileW},
handleapi::INVALID_HANDLE_VALUE,
minwinbase::WIN32_FIND_DATAW,
winnt::FILE_ATTRIBUTE_DIRECTORY,
};
#[cfg(target_os = "windows")]
fn read_dir<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
if path.as_ref().is_file() {
bail!("Path is not directory");
}
let path = path.as_ref().join("*");
let path = path.to_str().ok_or(anyhow!("No path str"))?;
let mut fd = unsafe { mem::MaybeUninit::<WIN32_FIND_DATAW>::zeroed().assume_init() };
let handle = unsafe { FindFirstFileW(encode(&path).as_ptr(), &mut fd) };
if handle == INVALID_HANDLE_VALUE {
bail!("Invalid handle value");
}
let mut v = vec![];
while unsafe { FindNextFileW(handle, &mut fd) } != 0 {
if fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == 0 {
let file_name = decode(&fd.cFileName);
v.push(file_name);
}
}
unsafe { FindClose(handle) };
Ok(v)
}
fn encode(source: &str) -> Vec<u16> {
source.encode_utf16().chain(Some(0)).collect()
}
fn decode(source: &[u16]) -> String {
let mut s = String::with_capacity(260);
for c in decode_utf16(source.iter().take_while(|&c| c != &0).cloned()) {
let c = c.unwrap_or(REPLACEMENT_CHARACTER);
s.push(c);
}
s.shrink_to_fit();
s
}
Linux
use anyhow::{anyhow, bail, Result};
use libc;
use std::ffi::CString;
use std::path::Path;
use std::slice;
use std::str;
#[cfg(target_os = "linux")]
fn read_dir<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let path = path.as_ref().to_str().ok_or(anyhow!("No path str"))?;
let path = CString::new(path)?;
let dir = unsafe { libc::opendir(path.as_ptr()) };
if dir.is_null() {
bail!("Dir in null");
}
let mut v = vec![];
loop {
let entry = unsafe { libc::readdir(dir) };
if entry.is_null() {
break;
}
let file_name = unsafe {
let name = (*entry).d_name.as_ptr();
let len = libc::strlen(name) as usize;
let slice = slice::from_raw_parts(name as *const u8, len);
str::from_utf8_unchecked(slice as &[u8]).into()
};
v.push(file_name);
}
unsafe { libc::closedir(dir) };
Ok(v)
}
ベンチマーク
単位は msec です。
Windows
Rust(ダメ) | Rust(良い) | Rust(API) | JavaScript |
---|---|---|---|
860 | 53 | 39 | 41 |
Linux
Rust(ダメ) | Rust(良い) | Rust(API) | JavaScript |
---|---|---|---|
82 | 21 | 14 | 33 |
やったぜ