2015年4月22日

js-ctypes で JIT (2) - 関数呼び出しのコストを抑える

js-ctypes では関数の呼び出しごとに引数、返り値の型のチェックや変換を行うので、その処理をなんとか省くことができれば結構高速に関数を呼ぶことができます。

今回は、ファイルを開いて1文字ずつ読み込む、というなんともヒドいコードを用いて、何度も呼び出されるコードを JIT を使って高速化してみましょう。対象とするコードがこちら。

let libc = ctypes.open(ctypes.libraryName("c"));

function withoutJit(fd, read) {
  let s = "";
  let buf = ctypes.char();
  let bufp = buf.address();
  for (;;) {
    let size = read(fd, bufp, 1);
    if (size <= 0)
      break;
    s += String.fromCharCode(buf.value);
  }
  return s;
}

let O_RDONLY = 0x0000;
let open = libc.declare("open", ctypes.default_abi, ctypes.int,
                        ctypes.char.ptr, ctypes.int);
let close = libc.declare("close", ctypes.default_abi, ctypes.int,
                         ctypes.int);
let read = libc.declare("read", ctypes.default_abi, ctypes.ssize_t,
                        ctypes.int, ctypes.voidptr_t, ctypes.size_t);

let fd = open("input", O_RDONLY);
let started = elapsed();
let s = withoutJit(fd, read);
print("withoutJit: " + (elapsed() - started));
close(fd);

libc.close();

今回、read がファイルのバイト数の分だけ呼び出されますので、そのたびに引数と返り値のチェック、変換が発生します。適当なファイル (3,143,051 バイト) に対して実行した結果がこちら。

withoutJit: 7722383

引数と返り値が少なければ少ないほど必要な処理は減るので、両方ナシにしてしまいましょう。そのためには、生成する JIT のコードに引数と返り値の即値を埋め込みます。

手順としては、まず参考にするマシン語を生成するコードを書きます。外側から放り込む値のところは見やすいように適当なパターンを埋め込んでおきます。今回は、read のアドレス、引数のファイルディスクリプタ、バッファのアドレス、サイズ、そして返り値を受け取る変数のアドレスの5種類です。

#include <unistd.h>

void
dummy(void) {
  int (*f)(int fildes, void *buf, size_t nbyte);
  int* v;
  f = (int (*)(int fildes, void *buf, size_t nbyte))0x7878787878787878ULL;
  v = (int*)0x9a9a9a9a9a9a9a9aULL;
  *v = f(0x1212, (void*)0x3434343434343434ULL, 0x5656565656565656ULL);
}

これをコンパイルすると必要なマシン語が得られます (今回は x86_64 用です)。

$ clang -O3 -c a.cc
$ otool -tVj a.o
a.o:
(__TEXT,__text) section
__Z5dummyv:
0000000000000000        55                      pushq   %rbp
0000000000000001        4889e5                  movq    %rsp, %rbp
0000000000000004        48be3434343434343434    movabsq $0x3434343434343434, %rsi ## imm = 0x3434343434343434
000000000000000e        48ba5656565656565656    movabsq $0x5656565656565656, %rdx ## imm = 0x5656565656565656
0000000000000018        48b87878787878787878    movabsq $0x7878787878787878, %rax ## imm = 0x7878787878787878
0000000000000022        bf12120000              movl    $0x1212, %edi           ## imm = 0x1212
0000000000000027        ffd0                    callq   *%rax
0000000000000029        48b99a9a9a9a9a9a9a9a    movabsq $-0x6565656565656566, %rcx ## imm = 0x9A9A9A9A9A9A9A9A
0000000000000033        8901                    movl    %eax, __Z5dummyv(%rcx)  ## dummy()
0000000000000035        5d                      popq    %rbp
0000000000000036        c3                      retq

これを前回と同じように mmap で確保したバッファに書き込み、呼び出して完成です。コード生成途中に memcpy を沢山呼んでいるのはもうちょっと最適化できるかとは思います。

let libc = ctypes.open(ctypes.libraryName("c"));

const PROT_WRITE = 0x02;
const PROT_EXEC = 0x04;
const MAP_ANON = 0x1000;
const MAP_PRIVATE = 0x0002;
let memcpy = libc.declare("memcpy", ctypes.default_abi,
                          ctypes.voidptr_t,
                          ctypes.voidptr_t, ctypes.voidptr_t, ctypes.size_t);

let mmap = libc.declare("mmap", ctypes.default_abi,
                        ctypes.voidptr_t,
                        ctypes.voidptr_t, ctypes.size_t, ctypes.int, ctypes.int,
                        ctypes.int, ctypes.off_t);
let munmap = libc.declare("munmap", ctypes.default_abi,
                          ctypes.int,
                          ctypes.voidptr_t, ctypes.size_t);

function withJit(fd, read) {
  let code = ctypes.uint8_t.array()([
    /* 00 */  0x55,                        // pushq   %rbp
    /* 01 */  0x48, 0x89, 0xe5,            // movq    %rsp, %rbp
    /* 04 */  0x48, 0xbe, 0,0,0,0,0,0,0,0, // movabsq BUF, %rsi
    /* 0e */  0x48, 0xba, 0,0,0,0,0,0,0,0, // movabsq NBYTE, %rdx
    /* 18 */  0x48, 0xb8, 0,0,0,0,0,0,0,0, // movabsq READ, %rax
    /* 22 */  0xbf, 0,0,0,0,               // movl    FILDES, %edi
    /* 27 */  0xff, 0xd0,                  // callq   *%rax
    /* 29 */  0x48, 0xb9, 0,0,0,0,0,0,0,0, // movabsq LEN, %rcx
    /* 33 */  0x89, 0x01,                  // movl    %eax, (%rcx)
    /* 35 */  0x5d,                        // popq    %rbp
    /* 36 */  0xc3,                        // retq
  ]);
  let fdv = ctypes.int(fd);
  let buf = ctypes.char();
  let bufp = ctypes.char.ptr(buf.address());
  let nbytes = ctypes.size_t(1);
  let result = ctypes.int();
  let resultp = ctypes.int.ptr(result.address());

  memcpy(code.addressOfElement(0x06), bufp.address(), bufp.constructor.size);
  memcpy(code.addressOfElement(0x10), nbytes.address(), nbytes.constructor.size);
  memcpy(code.addressOfElement(0x1a), read.address(), read.constructor.size);
  memcpy(code.addressOfElement(0x23), fdv.address(), fdv.constructor.size);
  memcpy(code.addressOfElement(0x2b), resultp.address(), resultp.constructor.size);

  let funcType = ctypes.FunctionType(ctypes.default_abi, ctypes.void_t);

  let mem = mmap(null, code.length,
                 PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE,
                 -1, 0);
  memcpy(mem, code, code.length);
  let func = ctypes.cast(mem, funcType.ptr);

  let s = "";
  for (;;) {
    func();
    if (result.value <= 0)
      break;
    s += String.fromCharCode(buf.value);
  }
  func = null;
  munmap(mem, code.length);

  return s;
}

let O_RDONLY = 0x0000;
let open = libc.declare("open", ctypes.default_abi, ctypes.int,
                        ctypes.char.ptr, ctypes.int);
let close = libc.declare("close", ctypes.default_abi, ctypes.int,
                         ctypes.int);
let read = libc.declare("read", ctypes.default_abi, ctypes.ssize_t,
                        ctypes.int, ctypes.voidptr_t, ctypes.size_t);

let fd = open("input", O_RDONLY);
let started = elapsed();
let s = withJit(fd, read);
print("withJit: " + (elapsed() - started));
close(fd);

libc.close();

これをさきほどと同じファイルで実行した結果がこちら。約 1/3 ほどの時間になっています。

withJit: 2331485

0 件のコメント:

コメントを投稿