CPPYY 與 Cython / Pypy / numba的速度對比 PART III
以vec_dot 為例,測試以下幾種不同方法的性能
1. 純python
def vec_dot(vec1, vec2):
if len(vec1) != len(vec2):
return []
size = len(vec1)
ret = []
for i in range(size):
ret.append(vec1[i] * vec2[i])
return ret
import random
import numpy as np
import array
vec1_arr = np.random.rand(1000)
vec2_arr = np.random.rand(1000)
vec1 = vec1_arr.tolist()
vec2 = vec2_arr.tolist()
vec1_parr = array.array("d", vec1)
vec2_parr = array.array("d", vec2)
result = [("測試方法", "耗費(fèi)時間us")]
%timeit vec_dot(vec1, vec2)
111 μs ± 1.02 μs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
result.append(("純python", 111))
# 使用np.ndarray 有額外的開銷
%timeit vec_dot(vec1_arr, vec2_arr)
420 μs ± 28.6 μs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit vec_dot(vec1_parr, vec2_parr)
164 μs ± 12 μs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
2. Cython
%load_ext cython
%%cython --cplus
import numpy as np
import cython
# same code as python version
def vec_dot_cython(vec1, vec2):
if len(vec1) != len(vec2):
return []
size = len(vec1)
ret = []
for i in range(size):
ret.append(vec1[i] * vec2[i])
return ret
# cython 的memoryview 可以用numpy.array 來初始化,或者使用python的memoryview/array.array對象
@cython.boundscheck(False)
@cython.wraparound(False)
cdef _vec_dot_cython2(double[:] vec1, double[:] vec2, double[:] ret):
cdef int size = vec1.size, i
if size == vec2.size:
for i in range(size):
ret[i] = vec1[i] * vec2[i]
def vec_dot_cython2(vec1, vec2, ret):
_vec_dot_cython2(vec1, vec2, ret)
結(jié)果:
使用cython 而未對python代碼做任何改動,就獲得了數(shù)倍的加速效果
%timeit vec_dot_cython(vec1, vec2)
34.4 μs ± 606 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
result.append(("cython沒有類型注釋", 34.4))
添加變量類型注釋之后的cython結(jié)果
ret = np.empty_like(vec1_arr)
%timeit vec_dot_cython2(vec1_arr, vec2_arr, ret)
3.54 μs ± 99.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
result.append(("cython有類型注釋", 3.54))
3. Pypy
使用pypy不改變?nèi)魏未a
%%pypy
def vec_dot(vec1, vec2):
if len(vec1) != len(vec2):
return []
size = len(vec1)
ret = []
for i in range(size):
ret.append(vec1[i] * vec2[i])
return ret
from random import random
from timeit import Timer
vec1 = [random() for _ in range(1000)]
vec2 = [random() for _ in range(1000)]
vec_dot(vec1, vec2)
timer1 = Timer("vec_dot(vec1, vec2)", "from __main__ import vec_dot, vec1, vec2")
print("pypy3: %s us" % (1000 * timer1.timeit(1000)))
pypy3: 12.6938 us
result.append(("pypy", 12.68))
4. numba
numba 簡直要超神啊
import numba
@numba.jit(nopython=True)
def vec_dot_numba(vec1, vec2, ret):
if vec1.shape[0] != vec2.shape[0]:
return
size = vec1.shape[0]
for i in range(size):
ret[i] = vec1[i] * vec2[i]
%timeit vec_dot_numba(vec1_arr, vec2_arr, ret)
887 ns ± 13.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
result.append(("numbaJIT", 0.887))
5. cppyy
出乎意料,竟然最慢, 有可能是因?yàn)関ector太慢的原因
import cppyy
cppyy.cppdef("""
void vec_dot_cppyy(const std::vector<double> &vec1, const std::vector<double> &vec2, std::vector<double> &ret){
if (vec1.size() != vec2.size()) return;
int size = vec1.size();
for (int i=0;i<size;i++){
ret[i] = vec1[i] * vec2[i];
}
}
""")
True
vec_dot_cppyy = cppyy.gbl.vec_dot_cppyy
Vector = cppyy.gbl.std.vector
vec1_cpp = Vector[float](vec1)
vec2_cpp = Vector[float](vec2)
%timeit vec_dot_cppyy(vec1, vec2)
140 μs ± 35.7 μs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
result.append(("cppyy c++ vector", 140))
6. 看看numpy的速度
%timeit np.dot(vec1_arr, vec2_arr)
1.23 μs ± 8.6 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
result.append(("numpy", 1.23))
總結(jié)
import pandas as pd
df = pd.DataFrame(result[1:], columns=result[0])
df.sort_values(by=result[0][1], inplace=True)
df.index = range(1, 8)
df

不同方法的耗時
- 可以看出來numbaJIT和numpy是一個量級的,速度幾乎差不太多
- cython即便沒有類型注釋,使用原模原樣的python代碼也可以加速很多
- cython加了類型注釋之后,速度也幾乎可以達(dá)到c的水平
- cppyy這種動態(tài)的模塊編譯方式,性能有損失,當(dāng)然也可能是因?yàn)槭褂昧藄td::vector