どのくらい遅いか

フォルダ内のファイル名を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)JavaScript860533941

Linux

Rust(ダメ)Rust(良い)Rust(API)JavaScript82211433

やったぜ

广告
将在 10 秒后关闭
bannerAds