我正在尝试使用 Cython 来提高一些代码的性能。但是,我获得的性能与 NumPy 版本基本相同。当使用 Numba 测试相同的代码时,结果明显快于 Cython,所以可能我没有正确使用 Cython。
以下是我迄今为止所尝试的简要概述:
{built-in method builtins.exec}
(也许这很重要,但我不确定如何正确解释它)。 这是重现我的结果的代码:
Numpy 版本(与 Numba 中显示的代码相同,但没有 @njit 装饰器):
import numpy as np
def numpy_optimize(psi: np.ndarray, y: np.ndarray) -> np.ndarray:
delta: float = 0.001
lam: float = 0.01
n_theta: int = psi.shape[1]
n_samples: int = psi.shape[0]
p: np.ndarray = np.eye(n_theta) / delta
xi: list = []
theta: list = 3*[[-0.0029283973561854606, 0.0032794468930772845, -0.003672579440695663]]
for i in range(2, n_samples):
psi_i = psi[i, :, None]# .reshape(-1, 1)
p_psi_i = p.dot(psi_i)
psi_i_T_p = psi_i.T.dot(p)
k_numerator = p_psi_i / lam
k_denominator = 1 + psi_i_T_p.dot(psi_i) / lam
k = k_numerator / k_denominator
xi.append(y[i, 0] - psi_i.T.dot(np.array(theta[-1]))[0])
theta.append(list(np.array(theta[-1]) + k.flatten() * np.array(xi[-1])))
p_update_factor = psi_i_T_p.dot(psi_i)
p -= (p_psi_i.dot(psi_i_T_p)) / (p_update_factor + lam)
p /= lam
return np.array(theta[-1])
Cython 版本:
%%cython
# Use the cell magic above to run in the notebook.
# The results are the same in the notebook as the one building cython code.
cimport cython
import numpy as np
cimport numpy as cnp
cnp.import_array()
def cython_optimize(cnp.ndarray[cnp.float64_t, ndim=2] psi, cnp.ndarray[cnp.float64_t, ndim=2] y):
cdef int i
cdef cnp.ndarray[cnp.float64_t, ndim=2] psi_i
cdef cnp.ndarray[cnp.float64_t, ndim=2] p_psi_i
cdef cnp.ndarray[cnp.float64_t, ndim=2] psi_i_T_p
cdef cnp.ndarray[cnp.float64_t, ndim=2] k
cdef cnp.ndarray[cnp.float64_t, ndim=2] p_update_factor
cdef cnp.ndarray[cnp.float64_t, ndim=2] p1
cdef cnp.ndarray[cnp.float64_t, ndim=2] p2
cdef cnp.ndarray[cnp.float64_t, ndim=2] p_numerator
cdef cnp.ndarray[cnp.float64_t, ndim=2] k_denominator
cdef int n_theta = psi.shape[1]
cdef int n = psi.shape[0]
cdef list theta = 3*[[-0.0029283973561854606, 0.0032794468930772845, -0.003672579440695663]]
cdef double delta = 0.001
cdef double lam = 0.01
cdef list xi = []
cdef cnp.ndarray[cnp.float64_t, ndim=2] p = np.eye(n_theta) / delta
for i in range(2, n):
psi_i = psi[i, :, None]# .reshape(-1, 1)
p_psi_i = p.dot(psi_i)
psi_i_T_p = psi_i.T.dot(p)
k_numerator = p_psi_i / lam
k_denominator = 1 + psi_i_T_p.dot(psi_i) / lam
k = k_numerator / k_denominator
xi.append(y[i, 0] - psi_i.T.dot(np.array(theta[-1]))[0])
theta.append(list(np.array(theta[-1]) + k.flatten() * np.array(xi[-1])))
p_update_factor = psi_i_T_p.dot(psi_i)
p -= (p_psi_i.dot(psi_i_T_p)) / (p_update_factor + lam)
p /= lam
return np.array(theta[-1]).reshape(-1, 1)
Numba 版本:
import numpy as np
from numba import njit
@njit
def numba_optimize(psi: np.ndarray, y: np.ndarray) -> np.ndarray:
delta: float = 0.001
lam: float = 0.01
n_theta: int = psi.shape[1]
n_samples: int = psi.shape[0]
p: np.ndarray = np.eye(n_theta) / delta
xi: list = []
theta: list = 3*[[-0.0029283973561854606, 0.0032794468930772845, -0.003672579440695663]]
for i in range(2, n_samples):
psi_i = psi[i, :, None]# .reshape(-1, 1)
p_psi_i = p.dot(psi_i)
psi_i_T_p = psi_i.T.dot(p)
k_numerator = p_psi_i / lam
k_denominator = 1 + psi_i_T_p.dot(psi_i) / lam
k = k_numerator / k_denominator
xi.append(y[i, 0] - psi_i.T.dot(np.array(theta[-1]))[0])
theta.append(list(np.array(theta[-1]) + k.flatten() * np.array(xi[-1])))
p_update_factor = psi_i_T_p.dot(psi_i)
p -= (p_psi_i.dot(psi_i_T_p)) / (p_update_factor + lam)
p /= lam
return np.array(theta[-1])
结果是:
# creating arrays to test the functions
import numpy as np
y = np.random.rand(1000000, 1)
psi = np.random.rand(1000000, 3)
%timeit cython_optimize(psi, y_train)
每个循环 16.3 秒 ± 164 毫秒(7 次运行的平均值 ± 标准差,每次 1 次循环)
%timeit numba_optimize(psi, y_train)
每个循环 1.7 秒 ± 18.8 毫秒(7 次运行的平均值 ± 标准差,每次 1 次循环)
%timeit numpy_optimize(psi, y_train)
每个循环 17.7 秒 ± 75.3 毫秒(7 次运行的平均值 ± 标准差,每次 1 次循环)
如果有人能在以下方面帮助我,我将不胜感激:
如有任何建议,我们将不胜感激。谢谢!
注:
numpy==1.26.0
、numba==0.59.1
和Cython==3.0.10
cimport cython
import cython
cimport numpy as np
import numpy as np
np.import_array()
from time import perf_counter
import cv2
# 4525 x 6623 x 3 picture https://www.pexels.com/pt-br/foto/foto-da-raposa-sentada-no-chao-2295744/
picpath = r"C:\Users\hansc\Downloads\pexels-alex-andrews-2295744.jpg"
pic=np.ascontiguousarray(cv2.imread(picpath))
cpdef tuple average_rgb_bad(np.ndarray[np.uint8_t, ndim=3] pic_original):
# NEVER LOOP THROUGH A NUMPY ARRAY LIKE THIS! NEVER EVER!
# Typing is actually not important in the function declaration
# The problem is here that you don't use a memory view! It is very slooooooooow
# Cython is such an awesome language, but the documentation is horrible.
# I struggled like you for a while, because the Cython documentation emphasizes
# the typing of NumPy arrays. It makes you think that using this,
# is the key to success: np.ndarray[np.uint8_t, ndim=3] But it isn't!
cdef:
Py_ssize_t shape0_pic = pic_original.shape[0]
Py_ssize_t shape1_pic = pic_original.shape[1]
Py_ssize_t i,j # Typing won't improve ANYTHING here!
# The whole function is crap, because we are not using MEMORY VIEWS!
Py_ssize_t resultcounter=shape0_pic*shape1_pic
Py_ssize_t r=0
Py_ssize_t g=0
Py_ssize_t b=0
#with nogil: IS NOT POSSIBLE HERE, because you are calling Python functions/methods __getitem__ etc.
start=perf_counter()
for i in range(shape0_pic):
for j in range(shape1_pic):
b+=pic_original[i][j][0]
g+=pic_original[i][j][1]
r+=pic_original[i][j][2]
print(perf_counter()-start)
return b//resultcounter, g//resultcounter, r//resultcounter
cpdef tuple average_rgb_good(np.ndarray pic_original):
cdef:
# this changes everything! Just this little, tiny line.
# This creates a typed memoryview, and it
# is blazingly fast! The function signature is very sloppily typed.
# But this is 10,000 TIMES FASTER THAN THE PREVIOUS FUNCTION!!!
# Think about it as it were a C pointer to an array
# Now you can use this memoryview and operate on the original array.
# If you make changes to the view, you will make changes to pic_original as well
cython.uchar[:,:,:] pic = pic_original # -> pure gold!
Py_ssize_t shape0_pic = pic.shape[0]
Py_ssize_t shape1_pic = pic.shape[1]
Py_ssize_t i,j
Py_ssize_t resultcounter=shape0_pic*shape1_pic
Py_ssize_t r=0
Py_ssize_t g=0
Py_ssize_t b=0
start=perf_counter()
with nogil: # there we go
for i in range(shape0_pic):
for j in range(shape1_pic):
b+=pic[i][j][0]
g+=pic[i][j][1]
r+=pic[i][j][2]
print(perf_counter()-start)
return b//resultcounter, g//resultcounter, r//resultcounter
cpdef tuple average_rgb_very_good(np.ndarray pic_original):
cdef:
# This is a little faster, but use it only if you can guarantee
# that you are passing a continuous array!!
# OpenCV, for example, does not guarantee continuous arrays when loading an image!!
#They are most of the time continuous, but sometimes not!
cython.uchar[:,:,::1] pic = pic_original
# Always remember: when working with arrays / np.ndarrays,
# you must somehow and somewhere use square brackets with colons!
# If you don't do that, your code will always be slow!!!
Py_ssize_t shape0_pic = pic.shape[0]
Py_ssize_t shape1_pic = pic.shape[1]
Py_ssize_t i,j
Py_ssize_t resultcounter=shape0_pic*shape1_pic
Py_ssize_t r=0
Py_ssize_t g=0
Py_ssize_t b=0
start=perf_counter()
with nogil:
for i in range(shape0_pic):
for j in range(shape1_pic):
b+=pic[i][j][0]
g+=pic[i][j][1]
r+=pic[i][j][2]
print(perf_counter()-start)
return b//resultcounter, g//resultcounter, r//resultcounter
print(average_rgb_very_good(pic))
print(average_rgb_good(pic))
print(average_rgb_bad(pic))
# compiled with C division
# 0.015835800004424527
# (76, 83, 85)
# 0.01636139999027364
# (76, 83, 85)
# Now it becomes very clear that we haven't even left Python land
# 123.76531680001062 # -> 10.000 times slower!
# cyimage2.pyx:37: RuntimeWarning: division with oppositely signed operands, C and Python semantics differ
# r+=pic_original[i][j][2]
# cyimage2.pyx:37: RuntimeWarning: division with oppositely signed operands, C and Python semantics differ
# r+=pic_original[i][j][2]
# cyimage2.pyx:37: RuntimeWarning: division with oppositely signed operands, C and Python semantics differ
# r+=pic_original[i][j][2]
# (-66, -59, -58)
# it is also important to use the right compiler directives
# Usually I use these:
comiler='''import numpy as np
from Cython.Compiler import Options
from setuptools import Extension, setup
from Cython.Build import cythonize
import sys
import platform
iswindows = "win" in platform.platform().lower()
numpyincludefolder = np.get_include()
name = "cyimage2"
Options.docstrings = False
Options.embed_pos_in_docstring = False
Options.generate_cleanup_code = False
Options.clear_to_none = True
Options.annotate = True
Options.fast_fail = False
Options.warning_errors = False
Options.error_on_unknown_names = True
Options.error_on_uninitialized = True
Options.convert_range = True
Options.cache_builtins = True
Options.gcc_branch_hints = True
Options.lookup_module_cpdef = False
Options.embed = False
Options.cimport_from_pyx = False
Options.buffer_max_dims = 8
Options.closure_freelist_size = 8
configdict = {
"py_limited_api": False,
"name": name,
"sources": [name + ".pyx"],
"include_dirs": [
numpyincludefolder,
],
"define_macros": [
("NPY_NO_DEPRECATED_API", 1),
("NPY_1_7_API_VERSION", 1),
("CYTHON_USE_DICT_VERSIONS", 1),
("CYTHON_FAST_GIL", 1),
("CYTHON_USE_PYLIST_INTERNALS", 1),
("CYTHON_USE_UNICODE_INTERNALS", 1),
("CYTHON_ASSUME_SAFE_MACROS", 1),
("CYTHON_USE_TYPE_SLOTS", 1),
("CYTHON_USE_PYTYPE_LOOKUP", 1),
("CYTHON_USE_ASYNC_SLOTS", 1),
("CYTHON_USE_PYLONG_INTERNALS", 1),
("CYTHON_USE_UNICODE_WRITER", 1),
("CYTHON_UNPACK_METHODS", 1),
("CYTHON_USE_EXC_INFO_STACK", 1),
("CYTHON_ATOMICS", 1),
],
"undef_macros": [],
"library_dirs": [],
"libraries": [],
"runtime_library_dirs": [],
"extra_objects": [],
"extra_compile_args": [],
"extra_link_args": [],
"export_symbols": [],
"swig_opts": [],
"depends": [],
"language": "c",
"optional": None,
}
compiler_directives = {
"binding": True,
"boundscheck": False,
"wraparound": False,
"initializedcheck": False,
"nonecheck": False,
"overflowcheck": False,
"overflowcheck.fold": False,
"embedsignature": False,
"embedsignature.format": "c", # (c / python / clinic)
"cdivision": True,
"cdivision_warnings": True,
"cpow": True,
"always_allow_keywords": False,
"c_api_binop_methods": False,
"profile": False,
"linetrace": False,
"infer_types": True,
"language_level": 3, # (2/3/3str)
"c_string_type": "bytes", # (bytes / str / unicode)
"c_string_encoding": "ascii", # (ascii, default, utf-8, etc.)
"type_version_tag": False,
"unraisable_tracebacks": True,
"iterable_coroutine": True,
"annotation_typing": True,
"emit_code_comments": True,
"cpp_locals": False,
"legacy_implicit_noexcept": False,
"optimize.use_switch": True,
"optimize.unpack_method_calls": True,
"warn.undeclared": True, # (default False)
"warn.unreachable": True, # (default True)
"warn.maybe_uninitialized": True, # (default False)
"warn.unused": True, # (default False)
"warn.unused_arg": True, # (default False)
"warn.unused_result": True, # (default False)
"warn.multiple_declarators": True, # (default True)
"show_performance_hints": True, # (default True)
}
compdi = configdict
clidict = compiler_directives
ext_modules = Extension(**configdict)
setup(
name=name,
ext_modules=cythonize(ext_modules, compiler_directives=compiler_directives),
)
'''