DragonFFIでPythonからCを呼ぶ

LLVMプロジェクトがDragonFFIなるFFIをリリースしたそうです。[ブログ](http://blog.llvm.org/2018/03/dragonffi-ffijit-for-c-language-using.html)にはPythonからC言語を呼んでいる例が紹介されています。

ちなみにFFIというのは、Foreign Function Interfaceの略で、あるプログラミング言語から別の言語で定義された関数を使うことができる仕組みです。例えばCの代替を目的に作られたRustは、Cを呼ぶためのFFIが実装されています。

DragonFFIのPython Wrapperであるpydffiを使って、ちょっと遊んでみました。

* [aguinet/dragonffi](https://github.com/aguinet/dragonffi)
* [今回の詳細なソースコード](https://github.com/ksakiyama/dragonffi_example)

# 環境セットアップ
Pythonはpyenvでインストールした"Python 3.6.3 :: Anaconda, Inc."を使いました。
ライブラリはpipで簡単にインストールできました。

```
pip install pydffi
```

# とりあえずフィボナッチでもやってみる
特にやらせたい事がなかったので、PythonとCでフィボナッチ数列の関数を実装してみました。
目論見としては、Cで書かれた関数は実行速度が速いはずなので、その数値を見れたら面白いかなーという感じです。

## Pythonの関数
```python
def f(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return f(n-1)+f(n-2)

print(f(30))
```

## pydffi

### Cの関数
こんな感じの関数fを定義します。

```c
int f(int n) {
  if (n == 0) {
    return 0;
  } else if (n == 1) {
    return 1;
  } else {
    return f(n-1) + f(n-2);
  }
}
```

### PythonからpydffiでCを呼ぶ
Cで定義した関数fをPythonからコールします。pydffiを使えば、すごくシンプルに書けました！

```python
import pydffi

with open("cfunc/fibonacci_opt.c") as f:
    c_src = "".join(f.readlines())

F = pydffi.FFI()
CU = F.compile(c_src)
print(int(CU.funcs.f(30)))
```

## 結果
N=30で実行。なんの最適化もしてない関数なので、当たり前ですがC言語の方が速いです。

| Pythonの関数 | Cの関数(pydffi) |
|-----------:|------------:|
| 0.3381[sec] | 0.0496[sec] |

## ちょっと最適化してみる
ちなみにメモ化でPythonの方を最適化するとこんな具合です。（速い！）
アルゴリズム大事ですねー。

| Python(メモ化) |
|-----------:|
| 0.00005[sec] |

```python
memo = [0] * 1000

def f(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1

    if memo[n]:
        return memo[n]

    m = f(n-1)+f(n-2)
    memo[n] = m
    return m

print(f(30))
```

# MatMulでも試してみた
もうひとつくらいサンプルがほしかったので、行列の掛け算、いわゆるmatmulを実装してみました。今回はnumpyも比較対象です。

## Pythonの関数

```python
A = [random.random() for _ in range(N) for _ in range(N)]
B = [random.random() for _ in range(N) for _ in range(N)]
C = [0.0 for _ in range(N) for _ in range(N)]

for i in range(N):
    for j in range(N):
        for k in range(N):
            C[i * N + j] += A[i * N + k] * B[k * N + j]
```

## pydffi

### Cの関数

```c
void matmul(double *A, double *B, double *C, int N) {
  int i, j, k;
  for (i = 0; i < N; i++) {
    for (j = 0; j < N; j++) {
      for (k = 0; k < N; k++) {
        C[i * N + j] += A[i * N + k] * B[k * N + j];
      }
    }
  }
}
```

### PythonからCを呼ぶ

```python
# read c source
with open("cfunc/matmul.c") as f:
    c_src = "".join(f.readlines())

# initialize
FFI = pydffi.FFI()
CU = FFI.compile(c_src)

# create array objects & set values
arr_A = pydffi.CArrayObj(FFI.arrayType(FFI.DoubleTy, N*N))
arr_B = pydffi.CArrayObj(FFI.arrayType(FFI.DoubleTy, N*N))
arr_C = pydffi.CArrayObj(FFI.arrayType(FFI.DoubleTy, N*N))
for i in range(N*N):
    arr_A.set(i, A[i])
    arr_B.set(i, B[i])
    arr_C.set(i, 0.0)

# execute c matmul
start = time.time()
CU.funcs.matmul(arr_A, arr_B, arr_C, N)
print("C(FFI):{:.5f}[sec]".format(time.time() - start))
```

## numpy

```python
np_A = np.array(A).reshape(N, N)
np_B = np.array(B).reshape(N, N)
np_C = np.matmul(np_A, np_B)
```

## 結果

### N=256
やっぱりCの関数は速いですね。そしてnumpyが一番はやい、さすが。
まぁCは愚直に3重ループ回しているだけなので、もっと最適化の余地はありますけどね。
（⇒matmulの最適化に興味がある人は[ここ](http://int.main.jp/txt/matmul/)が参考になります）

| Pythonの関数 | Cの関数(pydffi) | numpy |
|-----------:|------------:|------------:|
| 7.1067[sec] | 0.0329[sec] | 0.0281[sec] |

### N=1024

| Pythonの関数 | Cの関数(pydffi) | numpy |
|-----------:|------------:|------------:|
| 計測なし | 7.4422[sec] | 0.0769[sec] |

## 最終的な結論
* Pythonから簡単にCが呼べる
    * ctypesみたいにDLLも呼べるはず（まだ調べてない）
* **numpyはやい！**