You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

为何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)

核心原因分析

你的测试结果和书中结论相反,主要是这几个关键因素:

  1. Numpy的底层优化远超基础Cython循环
    Numpy的np.clip不是普通的C循环——它底层用了SIMD指令集(比如SSE/AVX),一次能处理8-16个双精度浮点数,相当于把你的标量循环变成了向量并行操作。而你的Cython代码是逐个元素处理的纯标量循环,哪怕禁用了边界检查和负索引,也没法和SIMD的并行效率比。

  2. 编译优化等级差异
    Numpy在编译时默认启用了最高级别的编译器优化(-O3-march=native等),会自动做循环展开、分支预测优化、CPU指令集适配。而你的setup.py用了distutils的默认编译选项,通常是-O1甚至更低,没有充分利用你的CPU性能。

  3. 分支预测与内存效率
    你的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

火山引擎 最新活动