地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【Linux】100万ファイルくらいあるディレクトリのファイルのリストを高速に表示したい

概要

ファイル名のリストだけ高速に欲しいみたいな場合に大量にファイルがあるディレクトリで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を付けることでソート処理をしなくなるのでやってみるとこれで大分速くなった。

linuxjm.osdn.jp

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を書いてみる

github.com

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回りの面倒を見たりするのに使われるらしい。

zenn.dev

あとはnewfstatat(2)もかなりの数呼んでいる。ioutil.ReadDir()の戻り値からファイルの情報を色々取得できることを考えれば当然といえば当然。C言語並みの速度に近づけるにはパッケージとかを使わずにシステムコールを直接呼び出すみたいなことをする必要がありそうだなという感じでした。