Cythonで添え字を型宣言しただけではPyPyと比べて速くない

  • アクセス対象がPythonのリストやarray、Numpyのarrayでは添え字オブジェクトの作成コストがかかる
  • アクセス対象がCの配列の場合は直接C的にアクセスできる、この時はPyPyより速い

実行時間まとめ

Python285 msOriginal
Cython316 msOriginal
PyPy82 msOriginal
Cython173 ms添え字を型宣言
Cython137 msリストを型宣言
Cython220 msリスト→array
CythonCEリスト→Numpyのnp.array
Cython25 msリスト→Cの配列
速度は全部AtCoderのコードテストで計測している

まず元のコード python

N = 1000_000 
xs = [0] * N
for i in range(N):
  xs[i] = i
for i in range(1, N):
  xs[i] += xs[i - 1]

print(xs[N - 1])

Python 285 ms Cython 316 ms PyPy 82 ms

添え字を型宣言 :

cdef long i

Cython 173 ms

  • 生Pythonより速くなったがPyPyにはかなわない

リストを型宣言 :

cdef long i
cdef list xs

Cython 137 ms

  • もう少し速くなるが、やはりPyPyにかなわない

なんでかというとこのxs[i]はCの配列アクセスなどとは全然違って

  • longの値からPyObjectを作成
  • 参照カウントを1増やす
  • それを引数にしてSetItemを呼ぶ
  • 合間で諸々のエラー処理をする という処理をしているから。 出力されたCのソースを読めばわかる。 $ cython -a -3 tiny_cython.pyx c
+07:     xs[i] = i
    __pyx_t_2 = __Pyx_PyInt_From_int(__pyx_v_11tiny_cython_i); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 7, __pyx_L1_error)
    __Pyx_GOTREF(__pyx_t_2);
    if (unlikely(__pyx_v_11tiny_cython_xs == Py_None)) {
      PyErr_SetString(PyExc_TypeError, "'NoneType' object is not subscriptable");
      __PYX_ERR(0, 7, __pyx_L1_error)
    }
    if (unlikely(__Pyx_SetItemInt(__pyx_v_11tiny_cython_xs, __pyx_v_11tiny_cython_i, __pyx_t_2, int, 1, __Pyx_PyInt_From_int, 1, 1, 1) < 0)) __PYX_ERR(0, 7, __pyx_L1_error)
    __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
  }

listの代わりにarrayを使ったら賢く処理してくれるかと思ったが、かえって遅くなった。初期化にリストを受け取るため、実質2回領域の確保が行われる。 python

cdef long i
from cpython cimport array
import array as pyarray

N = 1000_000 
cdef array.array xs = pyarray.array("l", [0] * N)
for i in range(N):
  xs[i] = i
for i in range(1, N):
  xs[i] += xs[i - 1]

print(xs[N - 1])

220 ms

  • これもCソースを観察したら添え字アクセスでPyIntを作っていた

リストではなくサイズで領域確保する方法としてnp.zerosが思いつくが… python

cdef long i
import numpy as np
cimport numpy as np

N = 1000_000 
cdef np.ndarray xs = np.zeros(N, np.int32)
for i in range(N):
  xs[i] = i
for i in range(1, N):
  xs[i] += xs[i - 1]

print(xs[N - 1])

なんとAtCoderのCythonではNumpyが使えない

Cの配列を作る cython

cdef long i
cdef long[1000_000] xs

N = 1000_000
for i in range(N):
  xs[i] = i
for i in range(1, N):
  xs[i] += xs[i - 1]

print(xs[N - 1])

25 ms

  • ついにPyPyより速くなった
  • Cソースを見ると単なる配列アクセスになっていることがわかる c
(__pyx_v_11tiny_cython_xs[__pyx_v_11tiny_cython_i]) = __pyx_v_11tiny_cython_i;
  • なおサイズを変数で指定することはできない模様 cython
cdef long N = 1000_000 
cdef long[N] xs

:

Main.pyx:3:11: Not allowed in a constant expression

もう一つ、malloc/freeを自分で呼ぶ方法も残されてる