readfile(2)で作成したcatのはやさ
まえがき
先日,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_operations
にread
は無く,read_iter
のみが設定されている.
よって,new_sync_read
でramfs_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に感謝.
Comments