Python
C
LLVM

DragonFFIでPythonからCを呼ぶ

LLVMプロジェクトがDragonFFIなるFFIをリリースしたそうです。ブログにはPythonからC言語を呼んでいる例が紹介されています。

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

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

環境セットアップ

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

pip install pydffi

とりあえずフィボナッチでもやってみる

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

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を定義します。

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を使えば、すごくシンプルに書けました!

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]
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の関数

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の関数

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を呼ぶ

# 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

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の最適化に興味がある人はここが参考になります)

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はやい!