readdirex 関数による速度改善 (Vim script)


vfiler.vim の速度改善を検討していた際に、readdirex() なる関数の存在を知りました。
プラグインを実装する上で、Vim 側の速度面でかなりの効果を発揮していくれたので、概要と簡単な速度比較を紹介します。

readdirex 関数の概要

使えるバージョン

かなり新しい関数です。
Vim 8.2.0875 から追加されました。

どのような関数か?

readdirex({directory} [, {expr} [, {dict}]])

{directory} で指定したディレクトリ内の、ファイルとディレクトリの属性をリストアップします。glob() と、getftype() 等のファイル情報の取得を含めて、一つの関数にまとめた感じです。

readdir 関数との違い

ドキュメントでは、readdirex()readdir() の拡張バージョンという位置付けになっています。readdir() は指定 directory 内のディレクトリ・ファイル名をリストで返すものですが、readdirex() は加えて、ファイル情報も含めて返してくれるものです。
vfiler.vim のようなファイル情報を扱いたい処理を実装する際には、readdirex() のような関数はまさに渡りに船と言えます。

ファイル読み込みの速度比較

サンプル

環境

  • OS: Windows
  • Vim: 8.2
  • Neovim: 0.6.1

検証用プログラム概要

  1. ファイル・ディレクトリが混在するディレクトリ以下を対象に、全てのファイル情報を取得
  2. 取得したファイル情報をディクショナリー(テーブル)として格納

Vim でのファイル読み込み実装

従来の実装

let l:fstats = []
for l:path in glob(a:dir . '/*', v:true, v:true)
  let l:fstat = {
        \ 'name': fnamemodify(l:path, ':t'),
        \ 'type': getftype(l:path),
        \ 'size': getfsize(l:path),
        \ 'time': getftime(l:path),
        \ 'perm': getfperm(l:path),
        \ }
  call add(l:fstats, l:fstat)
endfor

readdirex 関数による実装

let l:fstats = []
for l:dict in readdirex(a:dir)
  let l:fstat = {
        \ 'name': l:dict.name,
        \ 'type': l:dict.type,
        \ 'size': l:dict.size,
        \ 'time': l:dict.time,
        \ 'perm': l:dict.perm,
        \ }
  call add(l:fstats, l:fstat)
endfor

【参考】Neovim Lua でのファイル読み込み実装

参考として、Neovim Lua で同様のファイル読み込みの実装です。
vim.loop を使って、同期読み込みします。

local uv = vim.loop

local fstats = {}
local fd = uv.fs_scandir(dir)
while true do
  local name, type = uv.fs_scandir_next(fd)
  if not name then
    break
  end
  local path = dir .. '/' .. name
  local stat = uv.fs_stat(path)
  if stat then
    local fstat = {
      name = name,
      type = type,
      size = stat.size,
      time = stat.mtime.sec,
      perm = stat.mode
    }
    table.insert(fstats, fstat)
  end
end

速度比較結果

15,000 件のファイルディレクトリが混在したディレクトリを対象に、上記実装それぞれの処理速度を計測した結果です。

処理時間 (s) 時間比 [1]
従来実装 8.67 s -
readdirex 関数 0.19 s 45.44 倍
【参考】Neovim Lua 1.11 s 7.84 倍

ここでの結論と考察

readdirex() が圧倒的に早いです。その差は実に 45 倍 です。

ファイル情報の取得はけっこう時間のかかる処理です。多くのライブラリは、これらのファイル情報はまとめて取得できますが、これまでの Vim 関数は、サイズやタイプ等のファイル情報をそれぞれ個別に取得しており、都度アクセスが必要なためにオーバーヘッドがかかると思います。
加えて、従来の glob() でやってたことも含まれているので、使いどころがハマれば非常に強力な関数といえます。
これは、ディレクトリ内のファイルおよびディレクトリの数が多ければ多いほど、その恩恵をうけることができるでしょう。

参考として記載しましたが Neovim Lua も速いですね。readdirex() ほどではないですが、従来の Vim script と比べれば十分な速度です。readdirex() は、ドキュメントでは 特に MS-Windows で速い とあるので、他OSではもう少しその差は小さいかもしれません。
ちなみに、vfiler.vim では、Vim 側は readdirex() で、Neovim 側は vim.loop.scandir() で処理を変えることで、それぞれのベストパフォーマンスを出すようにしています。

脚注
  1. Vim script による従来実装と比較で何倍速いか ↩︎