livaの雑記帳

Rump Kernelのreadが遅くてハゲかけたので調査

後輩氏から、「Rump Kernelのreadシステムコール、素のNetBSDよりも遅いぞ!ハゲ!」と言われてしまい、まだハゲたくはないので調べたメモ。

ちなみに前提として、既に一度ブロックキャッシュに載ったデータをreadしている事とする。

 

 

追記:

この調査結果反映しても早くならなかったぞゴラァ!

って怒られました。

なので、話半分で読んでね。

 

 

 

とりあえず検証に際してRump Kernelに繋ぎたかったので、コンパイルオプションを変えた。

 

./build-rr.sh hw -- -F CFLAGS=-ggdb3 

 

これでgdbでシンボルを追える。

 

とりあえずRump Kernelのreadを掘っていった時のバックトレースがこちら。

#0  copyout (kaddr=0x7d15000, uaddr=0x491aa0 <buf>, len=4) at ./rumprun/src-netbsd/sys/rump/librump/rumpkern/rumpcopy.c:69

#1  0x00000000002e9ccc in copyout_vmspace (vm=0x7fdb000, kaddr=0x7d15000, uaddr=0x491aa0 <buf>, len=4) at ./rumprun/src-netbsd/sys/rump/librump/rumpkern/../../../kern/subr_copy.c:248

#2  0x00000000002e9df1 in uiomove (buf=<optimized out>, n=4, uio=0x7dc0d70) at ./rumprun/src-netbsd/sys/rump/librump/rumpkern/../../../kern/subr_copy.c:128

#3  0x000000000013e942 in ubc_uiomove (uobj=0x7e4d648, uio=0x7dc0d70, todo=4, advice=<optimized out>, flags=<optimized out>)

    at ./rumprun/src-netbsd/sys/rump/librump/rumpvfs/vm_vfs.c:208

#4  0x000000000014e1a1 in tmpfs_read (v=<optimized out>) at ./rumprun/src-netbsd/sys/rump/fs/lib/libtmpfs/../../../../fs/tmpfs/tmpfs_vnops.c:548

#5  0x00000000002cf1b9 in VOP_READ (vp=0x7d0a7e8, uio=<optimized out>, ioflag=<optimized out>, cred=<optimized out>)

    at ./rumprun/src-netbsd/sys/rump/librump/rumpkern/../../../kern/vnode_if.c:396

#6  0x000000000012652f in vn_read (fp=<optimized out>, offset=0x7cf7e80, uio=0x7dc0d70, cred=0x7f8bf00, flags=1)

    at ./rumprun/src-netbsd/sys/rump/librump/rumpvfs/../../../kern/vfs_vnops.c:557

#7  0x00000000002be7fa in dofileread (fd=fd@entry=3, fp=0x7cf7e80, buf=0x491aa0 <buf>, nbyte=4, offset=0x7cf7e80, flags=flags@entry=1, retval=0x7dc0e78)

    at ./rumprun/src-netbsd/sys/rump/librump/rumpkern/../../../kern/sys_generic.c:156

#8  0x00000000002be8d1 in sys_read (l=<optimized out>, uap=0x7dc0e88, retval=0x7dc0e78) at ./rumprun/src-netbsd/sys/rump/librump/rumpkern/../../../kern/sys_generic.c:121

#9  0x0000000000315354 in sy_call (rval=0x7dc0e78, uap=0x7dc0e88, l=0x7e85000, sy=<optimized out>) at ./rumprun/src-netbsd/sys/rump/librump/rumpkern/../../../sys/syscallvar.h:65

#10 sy_invoke (code=3, rval=0x7dc0e78, uap=0x7dc0e88, l=0x7e85000, sy=<optimized out>) at ./rumprun/src-netbsd/sys/rump/librump/rumpkern/../../../sys/syscallvar.h:94

#11 rump_syscall (num=num@entry=3, data=data@entry=0x7dc0e88, dlen=dlen@entry=24, retval=retval@entry=0x7dc0e78) at ./rumprun/src-netbsd/sys/rump/librump/rumpkern/rump.c:760

#12 0x000000000030a6f5 in rump___sysimpl_read (fd=<optimized out>, buf=<optimized out>, nbyte=<optimized out>) at ./rumprun/src-netbsd/sys/rump/librump/rumpkern/rump_syscalls.c:80

#13 0x000000000031fad8 in read (d=d@entry=3, buf=buf@entry=0x491aa0 <buf>, nbytes=nbytes@entry=4) at ./rumprun/src-netbsd/lib/libpthread/pthread_cancelstub.c:485

#14 0x0000000000110ee2 in benchmark_read (read_len=read_len@entry=4) at main.c:183

#15 0x000000000033f976 in main (argc=<optimized out>, argv=<optimized out>) at main.c:72

#16 0x0000000000318db6 in mainbouncer (arg=0x5eab10) at ./rumprun/lib/librumprun_base/rumprun.c:195

#17 0x000000000031e71d in pthread__create_tramp (cookie=0x7d12010) at ./rumprun/src-netbsd/lib/libpthread/pthread.c:576

#18 0x000000000010d7c8 in bmk_cpu_sched_bouncer ()

#19 0x0000000000000000 in ?? ()

デフォルトだとRump Kernelはインメモリなtmpfsの中にファイルシステムを構築する。

で、tmpfsの中からデータコピーをしている箇所がある。スタック#3のubc_uiomoveってのがそれ。僕の理解が間違って無ければ、kernel空間からuser空間へのデータコピーをこの関数で行う。readデータのメモリコピーはこの一回だけで済んでいるのだと思う。

 

素のNetBSDの場合はデフォルトではFFS(Fast File System)を使うんだけど、これでもubc_uiomoveを読んでるので、たぶん条件は同じ。

src/ufs_readwrite.c at netbsd_6_1 · NetBSD/src · GitHub

 

で、Rump Kernelにおいて、ubc_uiomoveが最終的に呼び出すのがスタック#0のcopyoutって奴だ。

 

./rumprun/src-netbsd/sys/rump/librump/rumpkern/rumpcopy.c:69

int

copyout(const void *kaddr, void *uaddr, size_t len)

{

        int error = 0;

 

        if (__predict_false(uaddr == NULL && len)) {

                return EFAULT;

        }

 

        if (RUMP_LOCALPROC_P(curproc)) {

                memcpy(uaddr, kaddr, len);

        } else if (len) {

                error = rump_sysproxy_copyout(RUMP_SPVM2CTL(curproc->p_vmspace),

                    kaddr, uaddr, len);

        }

        return error;

}

 

なんかmemcpyって奴を呼んでますね。

このmemcpy、gdbでは場所を特定できないので、筋肉使って探す。

 memcpy(Rump Kernelリンク時にrumpns_memcpyとなる)の逆アセンブリはこんな感じ。

(gdb) disas rumpns_memcpy

Dump of assembler code for function rumpns_memcpy:

   0x0000000000314350 <+0>: mov    %rdx,%rcx

   0x0000000000314353 <+3>: mov    %rdi,%rax

   0x0000000000314356 <+6>: mov    %rdi,%r11

   0x0000000000314359 <+9>: shr    $0x3,%rcx

   0x000000000031435d <+13>: je     0x31439c <rumpns_memcpy+76>

   0x000000000031435f <+15>: lea    -0x8(%rdi,%rdx,1),%r9

   0x0000000000314364 <+20>: mov    -0x8(%rsi,%rdx,1),%r10

   0x0000000000314369 <+25>: and    $0x7,%r11

   0x000000000031436d <+29>: jne    0x314376 <rumpns_memcpy+38>

   0x000000000031436f <+31>: rep movsq %ds:(%rsi),%es:(%rdi)

   0x0000000000314372 <+34>: mov    %r10,(%r9)

   0x0000000000314375 <+37>: retq   

   0x0000000000314376 <+38>: lea    -0x9(%r11,%rdx,1),%rcx

   0x000000000031437b <+43>: neg    %r11

   0x000000000031437e <+46>: mov    (%rsi),%rdx

   0x0000000000314381 <+49>: mov    %rdi,%r8

   0x0000000000314384 <+52>: lea    0x8(%rsi,%r11,1),%rsi

   0x0000000000314389 <+57>: lea    0x8(%rdi,%r11,1),%rdi

   0x000000000031438e <+62>: shr    $0x3,%rcx

   0x0000000000314392 <+66>: rep movsq %ds:(%rsi),%es:(%rdi)

   0x0000000000314395 <+69>: mov    %rdx,(%r8)

   0x0000000000314398 <+72>: mov    %r10,(%r9)

   0x000000000031439b <+75>: retq   

   0x000000000031439c <+76>: mov    %rdx,%rcx

   0x000000000031439f <+79>: rep movsb %ds:(%rsi),%es:(%rdi)

   0x00000000003143a1 <+81>: retq   

 

探してみるとこんなのがある。

src/bcopy.S at netbsd_6_1 · NetBSD/src · GitHub

これっぽい。

 

さて、素のNetBSDにおいて、ubc_uiomoveが呼び出すcopyoutはこれ。

src/copy.S at netbsd_6_1 · NetBSD/src · GitHub

copyoutの時点でアセンブラで書かれてる!てかループ使ってねぇ!(僕のアセンブラ力の低さがバレてしまう)

 

結論:

素のNetBSDのメモリコピーコードがきちんと最適化されていた。Rump Kernelの方が実装が素朴なので遅いんじゃないかと推測できる。

 

余談。

RumpとオリジナルNetBSDのread速度差にも二種類ある。

 

  1. readで読む際のデータサイズが大きい場合。
  2. readで読む際のデータサイズが小さい場合。

今回は前者の話。この場合、メモリコピー速度に依存する。

後者でもRumpの方が遅いんだけど、これは単純にシステムコールを呼び出す前にいろいろ前処理をしているから、そのオーバーヘッドがsysenter/syscall以上、っていう事なんじゃないかなぁ、なんて事を調べてるうちに思うようになった。