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
より若干速かったこと。user
と sys
は grep
の方が短かったので、Node.js はイベント待ちが少なかったようだ。非同期関数にしなかったのでイベントループの待機が短く済んだのだろうか。
ただ、拡張子で絞り込んでやると grep
の方が速かった。思いの外、Node.js で雑に書いたスクリプトが健闘しているな、ともとれる。
ということで、まとめ。
- Node.js スクリプトが Bash コマンドより速く動くワケない
- でもこんな雑なスクリプトでもそこまで極端な性能差も出ない (意外)
- 物事を比較する時は、条件が本当に同じかどうかよくよく確認しよう
正確な比較をしましょうね〜