为何在简单求和循环场景下Cython性能远低于Numba?
核心原因分析
向量化优化效率差异
Numba通过LLVM直接生成的汇编代码(你提供的片段)显示,它使用了AVX2的vaddpd指令,一次对4个double类型数据进行向量累加,并且做了循环展开(每次循环处理16个元素),充分利用CPU的SIMD指令集。而默认的Cython生成的C代码,即使开启-Ofast,编译器可能因为数组索引的计算方式(依赖数组strides),无法高效触发自动向量化,最终还是以标量循环执行,导致性能差距随数组规模扩大而增加。内存访问的额外开销
你的Cython代码中arr[1, i]的访问,会被转换成基于数组strides的地址计算:*(arr.data + 1*arr.strides[0] + i*arr.strides[1])。虽然strides是固定值,但编译器可能无法完全优化掉循环内的重复计算,而Numba会直接计算出第二行的起始内存地址,循环内仅做指针递增,内存访问更直接高效。浮点优化策略的对齐
Numba开启了fastmath=True,允许编译器进行激进的浮点近似优化(比如忽略NaN、重新排序浮点运算),而你的Cython仅在编译参数中加了-Ofast,但未在Cython指令中开启对应fastmath选项,导致编译器的浮点优化受限。
Cython代码优化方案
优化后的test.pyx代码
#cython: language_level=3, boundscheck=False, wraparound=False, fastmath=True cimport cython import numpy as np cimport numpy as np @cython.boundscheck(False) @cython.wraparound(False) def fast_sum(double[:, ::1] arr): cdef: int n = arr.shape[1] # 直接获取第二行的起始指针,避免循环内的strides计算 double *row_ptr = &arr[1, 0] double total = 0.0 int i # 遍历连续内存,编译器更容易触发向量化 for i in range(n): total += row_ptr[i] return total
优化setup.py编译参数
在extra_compile_args中明确添加循环展开和浮点优化选项,确保编译器最大化优化:
from setuptools import setup from Cython.Build import cythonize from setuptools.extension import Extension ext_modules = [ Extension( 'test_sum', language='c', sources=['test.pyx'], # 强化编译优化,匹配Numba的fastmath和SIMD优化 extra_compile_args=['-Ofast', '-march=native', '-ffast-math', '-funroll-loops'], ) ] setup( name = "test module", ext_modules = cythonize(ext_modules, compiler_directives={ 'language_level': "3", 'fastmath': True # 对齐Numba的fastmath设置 }) )
优化效果说明
通过直接获取第二行的内存指针,消除了循环内的strides计算开销,让编译器能更轻松地生成SIMD向量指令;同时开启Cython的fastmath并强化编译参数,对齐Numba的浮点优化策略,优化后的Cython版本性能会大幅接近Numba,甚至在大数组场景下持平。
内容的提问来源于stack exchange,提问作者Simd

