Information

実行時コード生成とは?

例えば、

double dot_product(double* v1, double* v2, int n) {
  double r = 0;
  for (int i = 0; i < n; i++) {
    r += v1[i] * v2[i];
  }
  return r;
}

という関数があるとする。実行時になって n == 3 であることが分かり、この計算を何度も行う場合には、

double dot_product_3(double* v1, double* v2) {
  return v1[0] * v2[0]
       + v1[1] * v2[1]
       + v1[2] * v2[2];
}

のような n == 3 に特化したコードを実行時に生成できる。

これにより、ループの条件判定や induction variable の更新などを取り除ける。 一方でコード生成自体にもコストがかかるため、生成したコードを十分に再利用できる場合に特に有効である。

Abstract

1 Introduction

2 Related Work

3 Language Overview

void (*fp)(void);
int x = 1;

/* Compile simple function to demonstrate binding via $ */
fp = compile(`{ printf("$x = %d, x = %d\n", $x, x); }, void);
x = 14;
(*fp)(); /* "$x = 1, x = 14" と表示する */

4 Architecture

4.1 Overview

Figure 1

4.2 Static compilation

Generating CGFs and Closures

tcc’s abstract machines

Static phases of dynamic code generation

4.3 Dynamic specification time

4.4 Dynamic compilation

Generating dynamic code

Automatic dynamic partial evaluation

例:内積

void cspec code;
int cspec sum = `0;

for (k = 0; k < n; k++)
    if (row[k])
        sum = `(sum + col[$k] * $row[k]);

code = `(return sum;);
/* sum は "a*b + c*d + ..." のようなコード */
`{
    int k, sum = 0;

    for (k = 0; k < $n; k++)
        if ($row[k])
            sum = sum + col[k] * $row[k];

    return sum;
}

が以下のような CGF になる。

for (k = 0; k < c->n; k++) {
    if (((int *)c->row)[k]) {
        /* row[k] == 0 ならコードを生成しない */
        ...
        /* col[k] * row[k] を計算するコードを emit */
        ...
    }
}

5 Dynamic Back Ends

5.1 VCODE-based code generation

5.2 ICODE-based code generation

Building a flow graph

Finding live intervals

Fast “Linear-scan” register allocation

Linear Scan Figure 3

Emitting code

6 Benchmarks

Figure 4

Figure 5

Figure 6

Figure 7

tickc-modernized で実際に何が生成されるか

以下では tickc-modernized の dot product 例を direct VCODE backend (-V) で追う。

全体の流れは次のようになる。

Tick-C source
  ↓ static compile
closure を組み立てる C + CGFs
  ↓ specification time
closure / cspec の連鎖
  ↓ compile()
CGFs を実行して VCODE macro を呼ぶ
  ↓
specialized x86-64 machine code

Tick-C source

void cspec
mkdot(int row[], int length)
{
    int index;
    int cspec sum = `0;
    int * vspec column = param(int *, 0);

    for (index = 0; index < length; index++) {
        if (row[index] != 0)
            sum = `((@column)[$index] * $row[index] + @sum);
    }
    return `{ return @sum; };
}

Static compiler が生成する C

Tick-C の quotation は、AST をそのまま runtime に構築するコードではなく、closure / cspec の連鎖を組み立てる C と CGF に変換される。実際の生成 C では、mkdot の loop の各反復で tc_aalloc により closure を確保し、そこへ column、前の sum、index、row[index] を保存している。

sum =
    ((_tc_cspec = ((void *)tc_aalloc(40))),
     /* CGF pointer */
     (*((unsigned long (**)(void *))_tc_cspec) =
          (unsigned long (*)(void *))_tc_cvi_1code),
     /* dynamic parameter */
     (*((_tc_vspec_t *)((char *)_tc_cspec + 16)) = column),
     /* previously constructed cspec */
     (*((_tc_cspec_t *)((char *)_tc_cspec + 24)) = sum),
     /* run-time constants */
     (*((int *)((char *)_tc_cspec + 32)) = index),
     (*((int *)((char *)_tc_cspec + 36)) = row[index]),
     _tc_cspec);

したがって specification time にできるのは machine code ではなく、後からコード生成するための closure graph / cspec chain である。

生成C は https://github.com/akawashiro/tickc-modernized/blob/013c80ce84d4c5cea8e6453e16e750e309501421/dot_product/vcode/dot_product.s.c.formatted#L29-L66 のようになっている。

_tc_cspec_t mkdot(int *row, int length) {
  _tc_closure_t _tc_cspec;
  int index;
  _tc_cspec_t sum =
      ((_tc_cspec = ((void *)tc_aalloc(16))),
       (*((unsigned long (**)(void *))((char *)((unsigned long)_tc_cspec))) =
            (unsigned long (*)(void *))_tc_cvi_0code),
       (*((unsigned int *)(((char *)((unsigned long)_tc_cspec)) + 8)) =
            (unsigned int)0),
       _tc_cspec);
  _tc_vspec_t column = _tc_paramf(8, 0, 0);
  for (index = 0; index < length; index++) {
    if (row[index] != 0)
      sum =
          ((_tc_cspec = ((void *)tc_aalloc(40))),
           (*((unsigned long (**)(void *))(
                (char *)((unsigned long)_tc_cspec))) =
                (unsigned long (*)(void *))_tc_cvi_1code),
           (*((unsigned int *)(((char *)((unsigned long)_tc_cspec)) + 8)) =
                (unsigned int)0),
           (*((_tc_vspec_t *)(((char *)((unsigned long)_tc_cspec)) + 16)) =
                (_tc_vspec_t)column),
           (*((_tc_cspec_t *)(((char *)((unsigned long)_tc_cspec)) + 24)) =
                (_tc_cspec_t)sum),
           (*((int *)(((char *)((unsigned long)_tc_cspec)) + 32)) = (int)index),
           (*((int *)(((char *)((unsigned long)_tc_cspec)) + 36)) =
                (int)(*(int *)((index << 2) + ((char *)((unsigned long)row))))),
           _tc_cspec);
  }
  return ((_tc_cspec = ((void *)tc_aalloc(24))),
          (*((void (**)(void *))((char *)((unsigned long)_tc_cspec))) =
               (void (*)(void *))_tc_cvi_2code),
          (*((unsigned int *)(((char *)((unsigned long)_tc_cspec)) + 8)) =
               (unsigned int)0),
          (*((_tc_cspec_t *)(((char *)((unsigned long)_tc_cspec)) + 16)) =
               (_tc_cspec_t)sum),
          _tc_cspec);
}

CGF

同じ生成 C には quotation ごとの CGF も含まれる。積和に対応する _tc_cvi_1code は、まず前の sum の CGF を呼び、その結果に今回の load / multiply / add を VCODE macro で追加する。このあたり に呼び出しているところがある。

il31 = _spec__1_0->code((void *)_spec__1_0);

/* column[index] */
v_ldii(il30, il30, (long)4 * _rtc__1_0);

/* * row[index] */
v_cmuli(il30, il30, (long)_rtc__1_1);

/* + previous sum */
v_addi(il31, il31, il30);

compile(mkdot(row, 6), int) では、この cspec chain に対応する CGF 群が呼ばれ、direct VCODE backend では VCODE macro がほぼ one-pass で x86-64 machine code を emit する。

Runtime に生成された machine code

compile() が runtime の executable buffer に生成したコードは runtime_debug/generated-code.objdump で確認できる。row = {2, 0, -3, 0, 4, 0} の場合、生成コードの本体は概ね次の処理になっている。

load column[0]
imul 2
add

load column[2]
imul -3
add

load column[4]
imul 4
add

return

実際の disassembly でも、column[0], column[2], column[4] に対応する offset 0, 8, 16 から load し、係数 2, -3, 4 を immediate operand として乗算している。元の for loop、終了判定、index の更新、row[index] == 0 の branch は generated code には残らない。

実際に生成された機械語は https://github.com/akawashiro/tickc-modernized/blob/dot-product-example/dot_product/vcode/runtime_debug/generated-code.objdump のようになっている。`jmp 命令などがなくループが展開されていることがわかる。


generated-code.bin:     file format binary


Disassembly of section .data:

0000000000000000 <.data>:
   0:	55                   	push   rbp
   1:	48 89 e5             	mov    rbp,rsp
   4:	53                   	push   rbx
   5:	41 54                	push   r12
   7:	41 55                	push   r13
   9:	41 56                	push   r14
   b:	41 57                	push   r15
   d:	48 81 ec 08 00 00 00 	sub    rsp,0x8
  14:	48 89 fb             	mov    rbx,rdi
  17:	48 89 5d d0          	mov    QWORD PTR [rbp-0x30],rbx
  1b:	bb 00 00 00 00       	mov    ebx,0x0
  20:	4c 8b 65 d0          	mov    r12,QWORD PTR [rbp-0x30]
  24:	45 8b 24 24          	mov    r12d,DWORD PTR [r12]
  28:	45 89 e0             	mov    r8d,r12d
  2b:	45 69 c0 02 00 00 00 	imul   r8d,r8d,0x2
  32:	45 89 c4             	mov    r12d,r8d
  35:	41 89 d8             	mov    r8d,ebx
  38:	45 89 e1             	mov    r9d,r12d
  3b:	45 01 c8             	add    r8d,r9d
  3e:	44 89 c3             	mov    ebx,r8d
  41:	4c 8b 65 d0          	mov    r12,QWORD PTR [rbp-0x30]
  45:	45 8b 64 24 08       	mov    r12d,DWORD PTR [r12+0x8]
  4a:	45 89 e0             	mov    r8d,r12d
  4d:	45 69 c0 fd ff ff ff 	imul   r8d,r8d,0xfffffffd
  54:	45 89 c4             	mov    r12d,r8d
  57:	41 89 d8             	mov    r8d,ebx
  5a:	45 89 e1             	mov    r9d,r12d
  5d:	45 01 c8             	add    r8d,r9d
  60:	44 89 c3             	mov    ebx,r8d
  63:	4c 8b 65 d0          	mov    r12,QWORD PTR [rbp-0x30]
  67:	45 8b 64 24 10       	mov    r12d,DWORD PTR [r12+0x10]
  6c:	45 89 e0             	mov    r8d,r12d
  6f:	45 69 c0 04 00 00 00 	imul   r8d,r8d,0x4
  76:	45 89 c4             	mov    r12d,r8d
  79:	41 89 d8             	mov    r8d,ebx
  7c:	45 89 e1             	mov    r9d,r12d
  7f:	45 01 c8             	add    r8d,r9d
  82:	44 89 c3             	mov    ebx,r8d
  85:	89 d8                	mov    eax,ebx
  87:	e9 00 00 00 00       	jmp    0x8c
  8c:	48 81 c4 08 00 00 00 	add    rsp,0x8
  93:	41 5f                	pop    r15
  95:	41 5e                	pop    r14
  97:	41 5d                	pop    r13
  99:	41 5c                	pop    r12
  9b:	5b                   	pop    rbx
  9c:	5d                   	pop    rbp
  9d:	c3                   	ret