grep コマンドより Node.js スクリプトの方が速い?

このサイトのコンテンツを更新するため、このサイトのソースコードがある Git ディレクトリ配下で、次のような Grep コマンドを叩いた。

$ grep -inR 'Target Keyword' ./src/pages

大量のファイルがあるにも関わらず、サラッと Grep 結果が表示されて、まぁ凄いなと思った。試しに time コマンドで実行時間を測ってみたらこんな感じだった。

real    0m1.202s
user    0m0.892s
sys     0m0.058s

その後、コンテンツの内容を置換していくために、次のような Node.js スクリプトを書いた。

const fs = require('fs');

// 指定のディレクトリ配下のファイルパスを再帰的に取得する
function listFiles(targetDirectoryPath) {
  return fs.readdirSync(targetDirectoryPath, { withFileTypes: true }).flatMap(dirent => {
    const name = `${targetDirectoryPath}/${dirent.name}`;
    return dirent.isFile() ? [name] : listFiles(name);
  });
}

listFiles('./src/pages')
  .filter(filePath => filePath.endsWith('.md') || filePath.endsWith('.html'))  // Markdown と HTML だけチェックする
  .forEach(filePath => {
    const contents = fs.readFileSync(filePath, 'utf-8');
    const lines = contents.split('\n');
    const matchLineIndex = lines.findIndex(line => line.match('Target Keyword'));  // Grep したい文言
    if(matchLineIndex < 0) return;
    const matchLine = lines[matchLineIndex];
    console.log(`${filePath}:${matchLineIndex + 1}:${matchLine}`);  // 一旦 `grep` コマンドと同等の結果を出力するのみ
  });

検索結果は grep コマンドと同じだった。grep コマンドも僕が書いたスクリプトも、どちらも検索の仕様として取りこぼし等はないようであった。

このスクリプトは、対象のディレクトリ配下の Markdown と HTML を、都度 fs.readFileSync() で読み込んでいる。ファイルを書き換えて保存していくつもりで、一回きりのスクリプトだったので性能は気にせず書いたのだが、grep 相当のこの時点の動作が異様に速く感じた。time コマンドで時間を測ってみると次のとおりだった。

real    0m0.168s
user    0m0.101s
sys     0m0.026s

grep コマンドで1.2秒かかっていたのが、0.1秒に…?

絶対こんなやり方非効率だろと思っていたのに、grep コマンドよりも速いだと?動作が速くて困ることはないのだが、どういう理屈なのか分からなくて考えていた。

そしてふと気が付き、grep コマンドにオプションを足して再実行してみた。

$ time grep -inR 'Target Keyword' ./src/pages --include='*.md' --include='*.html'
real    0m0.142s
user    0m0.088s
sys     0m0.018s

grep--include オプションを使って、検索対象ファイルの拡張子を絞り込んでみた。すると動作速度が格段に上がった。

なるほど、grep コマンドで拡張子のフィルタリングをしていなかったから、さっきの grep コマンドは遅かったんだ。

ということは、Node.js スクリプトで filter() している行をコメントアウトしてみたら遅くなるのか?

real    0m1.119s
user    0m0.934s
sys     0m0.082s

確かに遅くなった。

real の時間をまとめるとこうだ。

拡張子絞込 grep Node.js
なし 0m1.202s 0m1.119s
あり 0m0.142s 0m0.168s

意外なのが、Node.js で全ファイルを fs.readFileSync() するようにした実行結果が、grep より若干速かったこと。usersysgrep の方が短かったので、Node.js はイベント待ちが少なかったようだ。非同期関数にしなかったのでイベントループの待機が短く済んだのだろうか。

ただ、拡張子で絞り込んでやると grep の方が速かった。思いの外、Node.js で雑に書いたスクリプトが健闘しているな、ともとれる。

ということで、まとめ。

正確な比較をしましょうね〜