概要
ファイル名のリストだけ高速に欲しいみたいな場合に大量にファイルがあるディレクトリでlsを打って返ってこないみたいなのが地味にストレスになったりするので高速に済ませる手段が無いかを調べてみた。
- 1ディレクトリに100万ファイル程度
- 計測前に
echo 3 > /proc/sys/vm/drop_caches
を都度実行し10回程度計測
計測
ls -l
めっちゃ遅い
real 0m24.052s user 0m5.668s sys 0m8.071s
straceをしてみるとこんな感じ。-l
をつけるとメタデータを取りに行くのでこれが遅いらしい。sysが長い
% time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 44.63 7.080092 6 1020010 statx 19.87 3.152189 3 1020010 write 17.51 2.777625 2 1020010 1020010 lgetxattr 16.08 2.550146 2 1020012 1020012 getxattr 1.87 0.296908 297 998 getdents64 ------ ----------- ----------- --------- --------- ---------------- 100.00 15.862400 3 4081751 2040062 total
ls
-l
取るだけで大分早くなる
real 0m17.348s user 0m4.253s sys 0m5.998s
straceをしてみるとこんな感じ。コンソールへのwrite(2)が毎回走るのでこれが遅い
% time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 91.81 2.829277 2 1020009 write 8.11 0.249851 250 998 getdents64
リストを取れれば良いので結果をリダイレクトしてみる。これもだいぶ速くなっている
real 0m4.510s user 0m3.442s sys 0m0.380s
straceをするとgetdents64が高くなっている。ただsysの値が全体の割合から低くなっておりシステムコール関連ではなさそう
% time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 97.08 0.267859 268 998 getdents64 0.99 0.002735 1 2603 write 0.49 0.001361 5 245 brk 0.47 0.001300 56 23 close
lsのソートをなくす
デフォルトでソートしている。-f
を付けることでソート処理をしなくなるのでやってみるとこれで大分速くなった。
real 0m1.157s user 0m0.168s sys 0m0.312s
straceを見るとこんな感じ。sysが高いがこれ以上できそうなことは思いつかなかった
root@master001:/tmp/lstest2# strace -c ls -f -1 lstest > ls.lst % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 91.50 0.218414 218 998 getdents64 7.95 0.018988 7 2603 write 0.55 0.001305 56 23 close
最小のlsを書いてみる
lsを見てみるといろいろなパターンへ対応するべくの処理が多いのでこの辺取っ払って実装すれば早いのでは?と思って実装してみる。setvbufでbufferリングIOにしてwrite回数を減らすようにしてループで結果をだすという感じ
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <dirent.h> int main(int argc, char **argv) { DIR *dir; struct dirent *dp; char path[512]; if(setvbuf(stdout, NULL, _IOFBF, 2147483647) != 0 ){ printf("setvbuf error\n"); } if(argc<=1){ strcpy(path,"."); } else { strcpy(path,argv[1]); } if((dir=opendir(path))==NULL){ perror("opendir"); exit(1); } for(dp=readdir(dir);dp!=NULL;dp=readdir(dir)){ fprintf(stdout, "%s\n",dp->d_name); } }
平均して1~2msくらい速くなった。userもほぼほぼ時間がかかっていなくてsysもこれ以上減らすみたいなのはちょっと思いつかなかったのでこの辺が最速かなと思ったりしました。
real 0m1.047s user 0m0.069s sys 0m0.314s
Goでやってみる
Goだとどうなるのかを見てみました。実装はこんな感じ。
package main import ( "fmt" "io/ioutil" ) func main() { all, _ := ioutil.ReadDir("./lstest") for _, f := range all { fmt.Println(f.Name()) } }
実行してみると思っていたより遅い結果となりました。
real 0m9.366s user 0m2.245s sys 0m5.623s
sysが高いのでstraceを見てみる。futex(2)というのが高くなっている。これは恐らくgotoutineがマルチスレッドで動くからそれぞれでメモリのread/writeに対してロックを取ったりしているためだと思われる。
% time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ------------------ 41.80 3.683335 1377 2674 10 futex 40.23 3.544356 3 1020009 write 17.20 1.515669 7 194563 newfstatat 0.71 0.062810 82 762 getdents64
Goのruntimeの仕組み的に今回のようにシングルスレッドで頑張る系のワークロードだとスレッド間での協調のあたりがボトルネックになってしまって逆に遅くなってしまうみたいな感じだろうか?ちなみに今回上で書いたGoのコードですらOSから見たら5スレッド立ち上がっている。GCとかIO回りの面倒を見たりするのに使われるらしい。
あとはnewfstatat(2)もかなりの数呼んでいる。ioutil.ReadDir()の戻り値からファイルの情報を色々取得できることを考えれば当然といえば当然。C言語並みの速度に近づけるにはパッケージとかを使わずにシステムコールを直接呼び出すみたいなことをする必要がありそうだなという感じでした。