ふと vfork について検索したら、いくつか新しい話があったので、追記しておいた。 <URL:2014-09.html#a2014_09_06_4>
copy-on-write な fork では、 fork した時点で、親プロセスと子プロセスがメモリを read only で共有し、 write 時には page fault が起きてページをコピーして write 可能にした後、write を続行する。
ここで、子プロセスが exec した後は、そのメモリを使っているのは親プロセスだけになるのでコピーは不要になるが、 write しようとしたときに page fault が起きることは変わらない。
このあたりの動作は以下の NetBSD のドキュメントに動作が記述されている。 (もしかしたら、OSによっては微妙に動作が違うこともあるかもしれないけれど)
NetBSD ドキュメンテーション: なぜ伝統的な vfork()を実装したのか
この、親プロセスでメモリが read only になったままで、 書き込みのたびに page fault が起きるという動作は (コピーは起きないにせよ) かなり遅くなることもあるのではないか、 という疑問を感じた。
というわけで (Linux で) 試してみた。
% uname -mrsv Linux 4.19.0-2-amd64 #1 SMP Debian 4.19.16-1 (2019-01-17) x86_64
まず、Transparent huge page (THP) を disable する。 (Ruby 2.6 では内部で THP を disable している ([Feature #14705], r63253) のだが、Ruby 2.5 以前ではしてないので、これは 4K page という動作で揃えている)
# echo never > /sys/kernel/mm/transparent_hugepage/enabled
この状態で、大きなメモリに (各ページ毎に 1byte の) 書き込みを行う時間を測定した。 10回測定し、fork してからまた 10回測定する。
以下の結果をみると、明らかに fork 直後の回が 10倍くらい遅くなっている。
% ruby-2.6.2 -e '
pagesize = 4096
n = 100000
s = "x" * (n*pagesize)
10.times {
t1 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
n.times {|i| s.setbyte(i*pagesize,0) }
t2 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
t = t2-t1
printf "%.8f %s\n", t, "*"*(t*1000).to_i
}
puts
Process.wait fork {}
10.times {
t1 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
n.times {|i| s.setbyte(i*pagesize,0) }
t2 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
t = t2-t1
printf "%.8f %s\n", t, "*"*(t*1000).to_i
}'
0.00733914 *******
0.00699243 ******
0.00674534 ******
0.00777585 *******
0.00836382 ********
0.00729977 *******
0.00705439 *******
0.00687936 ******
0.00667030 ******
0.00662619 ******
0.06490101 ****************************************************************
0.00664682 ******
0.00662405 ******
0.00670339 ******
0.00657027 ******
0.00700166 *******
0.00666423 ******
0.00650362 ******
0.00669207 ******
0.00651883 ******
次に、fork のかわりに、system("true") を使うと、以下のように、fork と違って遅くならない。 これは、vfork を使っているからだろう。
% ruby-2.6.2 -e '
pagesize = 4096
n = 100000
s = "x" * (n*pagesize)
10.times {
t1 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
n.times {|i| s.setbyte(i*pagesize,0) }
t2 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
t = t2-t1
printf "%.8f %s\n", t, "*"*(t*1000).to_i
}
puts
system("true")
10.times {
t1 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
n.times {|i| s.setbyte(i*pagesize,0) }
t2 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
t = t2-t1
printf "%.8f %s\n", t, "*"*(t*1000).to_i
}'
0.00758229 *******
0.00701581 *******
0.00680411 ******
0.00673921 ******
0.00668358 ******
0.00671528 ******
0.00657470 ******
0.00660746 ******
0.00661486 ******
0.00670212 ******
0.00674013 ******
0.00700635 *******
0.00678113 ******
0.00657952 ******
0.00659784 ******
0.00678082 ******
0.00658584 ******
0.00661802 ******
0.00653398 ******
0.00654583 ******
vfork を使うようになったのは ruby 2.2 からなので、 ruby 2.1.10 で試すと、これは system でも遅くなる。
% ruby-2.1.10 -e '
pagesize = 4096
n = 100000
s = "x" * (n*pagesize)
10.times {
t1 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
n.times {|i| s.setbyte(i*pagesize,0) }
t2 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
t = t2-t1
printf "%.8f %s\n", t, "*"*(t*1000).to_i
}
puts
system("true")
10.times {
t1 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
n.times {|i| s.setbyte(i*pagesize,0) }
t2 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
t = t2-t1
printf "%.8f %s\n", t, "*"*(t*1000).to_i
}'
0.00678418 ******
0.00661911 ******
0.00631097 ******
0.00615733 ******
0.00612787 ******
0.00625961 ******
0.00616606 ******
0.00614166 ******
0.00615436 ******
0.00616540 ******
0.06327575 ***************************************************************
0.00622857 ******
0.00624134 ******
0.00617376 ******
0.00617710 ******
0.00638290 ******
0.00618077 ******
0.00615743 ******
0.00615407 ******
0.00613252 ******
ここで、Transparent huge page (THP) を enable する。
# echo always > /sys/kernel/mm/transparent_hugepage/enabled
すると、そんなに遅くならないようになる。 THP って効く (ことがある) のだな。
% ruby-2.1.10 -e '
pagesize = 4096
n = 100000
s = "x" * (n*pagesize)
10.times {
t1 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
n.times {|i| s.setbyte(i*pagesize,0) }
t2 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
t = t2-t1
printf "%.8f %s\n", t, "*"*(t*1000).to_i
}
puts
system("true")
10.times {
t1 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
n.times {|i| s.setbyte(i*pagesize,0) }
t2 = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
t = t2-t1
printf "%.8f %s\n", t, "*"*(t*1000).to_i
}'
0.00914092 *********
0.00847733 ********
0.00838931 ********
0.00775660 *******
0.00810754 ********
0.00699371 ******
0.00679120 ******
0.00660573 ******
0.00733054 *******
0.00661029 ******
0.00728163 *******
0.00663571 ******
0.00679257 ******
0.00704892 *******
0.00651190 ******
0.00643143 ******
0.00643470 ******
0.00717451 *******
0.00673541 ******
0.00647267 ******[latest]