まえがき

先日,readfile(2)を使ってcatコマンドを作成して遊んだ.

今回はせっかくなのでその速さを測ってみる.

この記事はLinuxその2 Advent Calendar 2020の19日目の記事です.

hyperfine

ベンチマークにはRust製のツール hyperfineを用いる.

これもsharkdp氏の作ったツール.

Rust製の有名なツール(fd, bat, hexylなど)を作っている方.お世話になってます.

cat

前回作成したreadfileによるcatのソースコードはこれ

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <err.h>
#define __NR_readfile   441

int main(int argc, char **argv) {
    if (argc < 2)
        return printf("Usage: %s [file]\n", *argv), 1;

    for (int i = 1; argv[i] != NULL; i++) {
        char buf[4096] = {0};
        int nread = 0;
        if ((nread = syscall(__NR_readfile, AT_FDCWD, argv[i], buf, 4096, O_NOFOLLOW)) == -1)
            err(1, "Error on readfile to %s", argv[i]);
        write(STDOUT_FILENO, buf, nread);
    }
}

実践

ディスクアクセスを考慮したくないのと,そもそもreadfile(2)sysfs等の読み取りを主なターゲットとしている事から,今回はtmpfs上にあるファイルを読み取る.

比較対象は当然cat(1)

環境は前回と同じVM.

vagrant@ubuntu-focal:~/cat_readfile$ ls tmp/ |wc -l
500000
vagrant@ubuntu-focal:~/cat_readfile$ df -h tmp
Filesystem      Size  Used Avail Use% Mounted on
tmpfs           3.9G  2.0G  2.0G  50% /home/vagrant/cat_readfile/tmp

適当なshellscriptでファイルを500,000個作成した.

#!/bin/bash
for i in `seq 1 500000`
do
    echo "hoge$i" >> "hoge$i";
done

./cat tmp/*の様にglobを用いて展開しようとすると,

-bash: ./cat: Argument list too long

てな感じに怒られるので,find tmp/ -type f| xargs ./catって感じで回避.

findを使っているので,念の為--warmupオプションを付けてhyperfineを実行しておく.

ディスクキャッシュの影響を考慮する時のオプションらしいけど,dentry cacheのこともあるので,念の為.

結果

vagrant@ubuntu-focal:~/cat_readfile$ hyperfine --warmup 3 "find tmp/ -type f| xargs ./cat" "find tmp/ -type f| xargs cat"
Benchmark #1: find tmp/ -type f| xargs ./cat
  Time (mean ± σ):      1.031 s ±  0.015 s    [User: 314.7 ms, System: 1033.6 ms]
  Range (min … max):    1.013 s …  1.057 s    10 runs

Benchmark #2: find tmp/ -type f| xargs cat
  Time (mean ± σ):      1.404 s ±  0.016 s    [User: 400.3 ms, System: 1336.7 ms]
  Range (min … max):    1.383 s …  1.425 s    10 runs

Summary
  'find tmp/ -type f| xargs ./cat' ran
    1.36 ± 0.03 times faster than 'find tmp/ -type f| xargs cat'

こんな感じ.

gccの謎の最適化

ちょっとコードに手を加える.

dog.c

// dog.c
// gcc dog.c -O3 -o dog
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <err.h>
#define __NR_readfile   441

int main(int argc, char **argv) {
    if (argc < 2)
        return printf("Usage: %s [file]\n", *argv), 1;

    for (int i = 1; argv[i] != NULL; i++) {
        char buf[11] = {0};
        int nread = 0;
        if ((nread = syscall(__NR_readfile, AT_FDCWD, argv[i], buf, 11, O_NOFOLLOW)) == -1)
            err(1, "Error on readfile to %s", argv[i]);
        write(STDOUT_FILENO, buf, nread);
    }
}

今回読むファイルサイズに合わせてバッファを小さくした.

予想

バッファサイズとread_fileへ渡す引数が小さくなった事で,read_file内で呼び出されるvfs_readの引数も小さくなる.

今回はtmpfsでのreadなので,file_operationsreadは無く,read_iterのみが設定されている.

よって,new_sync_readramfs_file_operations.read_iter(generic_file_read_iter)が呼び出される.

この中にループあったら変わったりしそうだなぁとか思ってカーネル潜ったけど,特に無さそうね.


バイナリ読んでみたらだいぶ変わってた.どっちも-O3なのにバッファサイズ変えただけでこんなになるとは.

コンパイラなんもわからん.これはこれで別の話なのでとりあえず無視.

cat(by readfile)

[0x000011f0]> pdf @main
            ; DATA XREF from entry0 @ 0x1211
            ;-- section..text:
            ;-- .text:
┌ 258: int main (signed int argc, char **argv);
│           ; var int64_t var_10h @ rsp+0x10
│           ; var int64_t var_1008h @ rsp+0x1008
│           ; arg signed int argc @ rdi
│           ; arg char **argv @ rsi
│           0x000010e0      f30f1efa       endbr64                     ; [16] -r-x section size 629 named .text
│           0x000010e4      4155           push r13
│           0x000010e6      4154           push r12
│           0x000010e8      55             push rbp
│           0x000010e9      53             push rbx
│           0x000010ea      4881ec001000.  sub rsp, sym._init          ; 0x1000
│           0x000010f1      48830c2400     or qword [rsp], 0
│           0x000010f6      4883ec18       sub rsp, 0x18
│           0x000010fa      64488b042528.  mov rax, qword fs:[0x28]
│           0x00001103      488984240810.  mov qword [var_1008h], rax
│           0x0000110b      31c0           xor eax, eax
│           0x0000110d      83ff01         cmp edi, 1                  ; argc
│       ┌─< 0x00001110      0f8e94000000   jle 0x11aa
│       │   0x00001116      488b5608       mov rdx, qword [rsi + 8]    ; argv
│       │   0x0000111a      488d5e08       lea rbx, [rsi + 8]          ; argv
│       │   0x0000111e      488d6c2410     lea rbp, [var_10h]
│       │   0x00001123      4989e4         mov r12, rsp
│       │   0x00001126      4531ed         xor r13d, r13d
│       │   0x00001129      4885d2         test rdx, rdx
│      ┌──< 0x0000112c      7459           je 0x1187
│      ││   0x0000112e      6690           nop
│      ││   ; CODE XREF from main @ 0x1185
│     ┌───> 0x00001130      4c89e8         mov rax, r13
│     ╎││   0x00001133      b9fe010000     mov ecx, 0x1fe
│     ╎││   0x00001138      4889ef         mov rdi, rbp
│     ╎││   0x0000113b      be9cffffff     mov esi, 0xffffff9c         ; 4294967196
│     ╎││   0x00001140      f348ab         rep stosq qword [rdi], rax
│     ╎││   0x00001143      660fefc0       pxor xmm0, xmm0
│     ╎││   0x00001147      31c0           xor eax, eax
│     ╎││   0x00001149      4c89e1         mov rcx, r12
│     ╎││   0x0000114c      41b900000200   mov r9d, 0x20000
│     ╎││   0x00001152      41b800100000   mov r8d, sym._init          ; 0x1000
│     ╎││   0x00001158      bfb9010000     mov edi, 0x1b9
│     ╎││   0x0000115d      0f290424       movaps xmmword [rsp], xmm0
│     ╎││   0x00001161      e84affffff     call sym.imp.syscall
│     ╎││   0x00001166      83f8ff         cmp eax, 0xffffffff
│    ┌────< 0x00001169      745c           je 0x11c7
│    │╎││   0x0000116b      4863d0         movsxd rdx, eax             ; size_t nbytes
│    │╎││   0x0000116e      4c89e6         mov rsi, r12                ; const char *ptr
│    │╎││   0x00001171      bf01000000     mov edi, 1                  ; int fd
│    │╎││   0x00001176      4883c308       add rbx, 8
│    │╎││   0x0000117a      e811ffffff     call sym.imp.write          ; ssize_t write(int fd, const char *ptr, size_t nbytes)
│    │╎││   0x0000117f      488b13         mov rdx, qword [rbx]
│    │╎││   0x00001182      4885d2         test rdx, rdx
│    │└───< 0x00001185      75a9           jne 0x1130
│    │ ││   ; CODE XREF from main @ 0x112c
│    │ └──> 0x00001187      31c0           xor eax, eax
│    │  │   ; CODE XREF from main @ 0x11c5
│    │ ┌──> 0x00001189      488b9c240810.  mov rbx, qword [var_1008h]
│    │ ╎│   0x00001191      6448331c2528.  xor rbx, qword fs:[0x28]
│    │┌───< 0x0000119a      7541           jne 0x11dd
│    ││╎│   0x0000119c      4881c4181000.  add rsp, 0x1018
│    ││╎│   0x000011a3      5b             pop rbx
│    ││╎│   0x000011a4      5d             pop rbp
│    ││╎│   0x000011a5      415c           pop r12
│    ││╎│   0x000011a7      415d           pop r13
│    ││╎│   0x000011a9      c3             ret
│    ││╎│   ; CODE XREF from main @ 0x1110
│    ││╎└─> 0x000011aa      488b16         mov rdx, qword [rsi]        ; argv
│    ││╎    0x000011ad      bf01000000     mov edi, 1
│    ││╎    0x000011b2      31c0           xor eax, eax
│    ││╎    0x000011b4      488d35490e00.  lea rsi, str.Usage:__s__file ; 0x2004 ; "Usage: %s [file]\n"
│    ││╎    0x000011bb      e810ffffff     call sym.imp.__printf_chk
│    ││╎    0x000011c0      b801000000     mov eax, 1
│    ││└──< 0x000011c5      ebc2           jmp 0x1189
│    ││     ; CODE XREF from main @ 0x1169
│    └────> 0x000011c7      488b13         mov rdx, qword [rbx]
│     │     0x000011ca      488d35450e00.  lea rsi, str.Error_on_readfile_to__s ; 0x2016 ; "Error on readfile to %s"
│     │     0x000011d1      bf01000000     mov edi, 1
│     │     0x000011d6      31c0           xor eax, eax
│     │     0x000011d8      e8e3feffff     call sym.imp.err
│     │     ; CODE XREF from main @ 0x119a
└     └───> 0x000011dd      e8befeffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)

dog(by readfile and minimal buffer size)

[0x000011c0]> pdf @main
            ; DATA XREF from entry0 @ 0x11e1
            ;-- section..text:
            ;-- .text:
┌ 223: int main (signed int argc, char **argv);
│           ; var int64_t var_dh @ rsp+0xd
│           ; var int64_t var_15h @ rsp+0x15
│           ; var int64_t var_17h @ rsp+0x17
│           ; var int64_t var_18h @ rsp+0x18
│           ; arg signed int argc @ rdi
│           ; arg char **argv @ rsi
│           0x000010e0      f30f1efa       endbr64                     ; [16] -r-x section size 581 named .text
│           0x000010e4      55             push rbp
│           0x000010e5      53             push rbx
│           0x000010e6      4883ec28       sub rsp, 0x28
│           0x000010ea      64488b042528.  mov rax, qword fs:[0x28]
│           0x000010f3      4889442418     mov qword [var_18h], rax
│           0x000010f8      31c0           xor eax, eax
│           0x000010fa      83ff01         cmp edi, 1                  ; argc
│       ┌─< 0x000010fd      0f8e84000000   jle 0x1187
│       │   0x00001103      488b5608       mov rdx, qword [rsi + 8]    ; argv
│       │   0x00001107      488d5e08       lea rbx, [rsi + 8]          ; argv
│       │   0x0000110b      488d6c240d     lea rbp, [var_dh]
│       │   0x00001110      4885d2         test rdx, rdx
│      ┌──< 0x00001113      7459           je 0x116e
│      ││   0x00001115      0f1f00         nop dword [rax]
│      ││   ; CODE XREF from main @ 0x116c
│     ┌───> 0x00001118      31c0           xor eax, eax
│     ╎││   0x0000111a      41b900000200   mov r9d, 0x20000
│     ╎││   0x00001120      4889e9         mov rcx, rbp
│     ╎││   0x00001123      be9cffffff     mov esi, 0xffffff9c         ; 4294967196
│     ╎││   0x00001128      6689442415     mov word [var_15h], ax
│     ╎││   0x0000112d      bfb9010000     mov edi, 0x1b9
│     ╎││   0x00001132      31c0           xor eax, eax
│     ╎││   0x00001134      41b80b000000   mov r8d, 0xb
│     ╎││   0x0000113a      48c744240d00.  mov qword [var_dh], 0
│     ╎││   0x00001143      c644241700     mov byte [var_17h], 0
│     ╎││   0x00001148      e863ffffff     call sym.imp.syscall
│     ╎││   0x0000114d      83f8ff         cmp eax, 0xffffffff
│    ┌────< 0x00001150      7452           je 0x11a4
│    │╎││   0x00001152      4863d0         movsxd rdx, eax             ; size_t nbytes
│    │╎││   0x00001155      4889ee         mov rsi, rbp                ; const char *ptr
│    │╎││   0x00001158      bf01000000     mov edi, 1                  ; int fd
│    │╎││   0x0000115d      4883c308       add rbx, 8
│    │╎││   0x00001161      e82affffff     call sym.imp.write          ; ssize_t write(int fd, const char *ptr, size_t nbytes)
│    │╎││   0x00001166      488b13         mov rdx, qword [rbx]
│    │╎││   0x00001169      4885d2         test rdx, rdx
│    │└───< 0x0000116c      75aa           jne 0x1118
│    │ ││   ; CODE XREF from main @ 0x1113
│    │ └──> 0x0000116e      31c0           xor eax, eax
│    │  │   ; CODE XREF from main @ 0x11a2
│    │ ┌──> 0x00001170      488b4c2418     mov rcx, qword [var_18h]
│    │ ╎│   0x00001175      6448330c2528.  xor rcx, qword fs:[0x28]
│    │┌───< 0x0000117e      753a           jne 0x11ba
│    ││╎│   0x00001180      4883c428       add rsp, 0x28
│    ││╎│   0x00001184      5b             pop rbx
│    ││╎│   0x00001185      5d             pop rbp
│    ││╎│   0x00001186      c3             ret
│    ││╎│   ; CODE XREF from main @ 0x10fd
│    ││╎└─> 0x00001187      488b16         mov rdx, qword [rsi]        ; argv
│    ││╎    0x0000118a      bf01000000     mov edi, 1
│    ││╎    0x0000118f      31c0           xor eax, eax
│    ││╎    0x00001191      488d356c0e00.  lea rsi, str.Usage:__s__file ; 0x2004 ; "Usage: %s [file]\n"
│    ││╎    0x00001198      e833ffffff     call sym.imp.__printf_chk
│    ││╎    0x0000119d      b801000000     mov eax, 1
│    ││└──< 0x000011a2      ebcc           jmp 0x1170
│    ││     ; CODE XREF from main @ 0x1150
│    └────> 0x000011a4      488b13         mov rdx, qword [rbx]
│     │     0x000011a7      488d35680e00.  lea rsi, str.Error_on_readfile_to__s ; 0x2016 ; "Error on readfile to %s"
│     │     0x000011ae      bf01000000     mov edi, 1
│     │     0x000011b3      31c0           xor eax, eax
│     │     0x000011b5      e806ffffff     call sym.imp.err
│     │     ; CODE XREF from main @ 0x117e
└     └───> 0x000011ba      e8e1feffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)

結果

vagrant@ubuntu-focal:~/cat_readfile$ hyperfine --warmup 3 "find tmp/ -type f| xargs ./cat" "find tmp/ -type f| xargs ./dog" "find tmp/ -type f| xargs cat"
Benchmark #1: find tmp/ -type f| xargs ./cat
  Time (mean ± σ):      1.025 s ±  0.014 s    [User: 299.9 ms, System: 1031.6 ms]
  Range (min … max):    0.999 s …  1.046 s    10 runs

Benchmark #2: find tmp/ -type f| xargs ./dog
  Time (mean ± σ):     998.8 ms ±  22.2 ms    [User: 294.2 ms, System: 1019.7 ms]
  Range (min … max):   974.8 ms … 1051.3 ms    10 runs

Benchmark #3: find tmp/ -type f| xargs cat
  Time (mean ± σ):      1.415 s ±  0.024 s    [User: 424.9 ms, System: 1328.8 ms]
  Range (min … max):    1.386 s …  1.445 s    10 runs

Summary
  'find tmp/ -type f| xargs ./dog' ran
    1.03 ± 0.03 times faster than 'find tmp/ -type f| xargs ./cat'
    1.42 ± 0.04 times faster than 'find tmp/ -type f| xargs cat'

さっきよりちょっとだけ差が広がった

dogは普通の(readfileじゃない)catより約1.4倍はやい.犬派としては嬉しい結果.

これ,結構差があるので,それこそリンカみたいなプログラムが早くなったりしないかなって思ったけどバッファサイズ固定なんだよな.

ファイルサイズをシュッと取得出来る様なファイルシステムとシステムコールがあれば良さそう.

まとめ

readfile(2)はシステムコールのオーバーヘッドを簡単に実感できる良い題材.

Greg K-Hに感謝.