为何Numpy的clip实现比Cython版本更快?(Python Cookbook示例)
为什么我的Cython实现比Numpy的
clip慢2倍? 先来看你提供的三个代码文件:
sample.pyx(Cython实现)
cimport cython @cython.boundscheck(False) @cython.wraparound(False) cpdef clip(double[:] a, double min, double max, double[:] out): if min > max: raise ValueError('min must be <= max') if a.shape[0] != out.shape[0]: raise ValueError('input and output arrays must be the same size!') for i in range(a.shape[0]): if a[i] < min: out[i] = min elif a[i] > max: out[i] = max else: out[i] = a[i]
setup.py(编译脚本)
from distutils.core import setup from Cython.Build import cythonize setup(ext_modules=cythonize("sample.pyx"))
main.py(测试脚本)
import numpy as np import time import sample b = np.random.uniform(-10, 10, size=1000000) a = np.zeros_like(b) since = time.time() np.clip(b, -5, 5, a) print(time.time() - since) since = time.time() sample.clip(b, -5, 5, a) print(time.time() - since)
核心原因分析
你的测试结果和书中结论相反,主要是这几个关键因素:
Numpy的底层优化远超基础Cython循环
Numpy的np.clip不是普通的C循环——它底层用了SIMD指令集(比如SSE/AVX),一次能处理8-16个双精度浮点数,相当于把你的标量循环变成了向量并行操作。而你的Cython代码是逐个元素处理的纯标量循环,哪怕禁用了边界检查和负索引,也没法和SIMD的并行效率比。编译优化等级差异
Numpy在编译时默认启用了最高级别的编译器优化(-O3、-march=native等),会自动做循环展开、分支预测优化、CPU指令集适配。而你的setup.py用了distutils的默认编译选项,通常是-O1甚至更低,没有充分利用你的CPU性能。分支预测与内存效率
你的Cython代码里有三个条件分支,虽然逻辑简单,但CPU的分支预测还是会有开销。而Numpy的实现可能用了掩码操作(比如通过位运算生成符合条件的掩码,直接批量赋值),避免了大量分支判断,内存访问也更连续高效。
改进方案:让Cython追上甚至超过Numpy
1. 提升编译优化等级
修改setup.py,添加最高级别的编译参数:
from distutils.core import setup from Cython.Build import cythonize setup( ext_modules=cythonize("sample.pyx"), extra_compile_args=['-O3', '-march=native', '-ffast-math'] )
-O3:开启所有优化(循环展开、函数内联等)-march=native:针对你的CPU型号生成最优指令-ffast-math:牺牲一点数学精度换更快的运算(如果你的场景允许)
2. 尝试并行化循环
如果你的CPU是多核的,可以用Cython的prange实现并行循环:
cimport cython from cython.parallel import prange @cython.boundscheck(False) @cython.wraparound(False) cpdef clip(double[:] a, double min, double max, double[:] out): if min > max: raise ValueError('min must be <= max') if a.shape[0] != out.shape[0]: raise ValueError('input and output arrays must be the same size!') # 用prange并行循环,注意需要在编译时加-openmp参数 for i in prange(a.shape[0], nogil=True): if a[i] < min: out[i] = min elif a[i] > max: out[i] = max else: out[i] = a[i]
对应的setup.py要添加OpenMP参数:
setup( ext_modules=cythonize("sample.pyx"), extra_compile_args=['-O3', '-march=native', '-ffast-math', '-fopenmp'], extra_link_args=['-fopenmp'] )
3. 尝试向量化写法
如果你想手动利用SIMD,可以用Cython的cimport numpy配合数组切片的批量操作,不过这个难度较高,通常前两个优化就足够让你的Cython代码接近甚至超过Numpy的速度了。
内容的提问来源于stack exchange,提问作者Tengerye




