14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rust でフォルダ内にあるすべてのファイル名を取得するのが遅い(それほど遅くない)

Last updated at Posted at 2020-04-29

##どのくらい遅いか
フォルダ内のファイル名をVecにまとめておきたい場面がありました。Rust で普通に実装すると下記のようになるでしょうか。ファイル数が少ないときは問題ないですが、私の環境では1万ファイル程度ある場合に 5~10sec も掛かってしまいました。
下記は:x:ダメな例です。(コメントで教えて頂きました)

Rust(ダメ)
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())
}

これ↓が:o:良いコード

Rust(良い)
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) では↓のようになる。

JavaScript
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 の場合、どちらもFindFirstFileWFindNextFileW関数を使用しています。
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に丸投げです。

Rust(API)
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

Rust(API)
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

やったぜ:blush:

14
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?