Python 在Cython与NumPy中求整数与浮点之和时,性能差异较大

Python 在Cython与NumPy中求整数与浮点之和时,性能差异较大,python,numpy,cython,Python,Numpy,Cython,我使用Cython或NumPy对一维数组中的每个元素求和。对整数求和时Cython的速度约为20%。当求和浮动时,Cython的速度约为的2.5倍。下面是使用的两个简单函数 #cython: boundscheck=False #cython: wraparound=False def sum_int(ndarray[np.int64_t] a): cdef: Py_ssize_t i, n = len(a) np.int64_t total = 0

我使用Cython或NumPy对一维数组中的每个元素求和。对整数求和时Cython的速度约为20%。当求和浮动时,Cython的速度约为的2.5倍。下面是使用的两个简单函数

#cython: boundscheck=False
#cython: wraparound=False

def sum_int(ndarray[np.int64_t] a):
    cdef:
        Py_ssize_t i, n = len(a)
        np.int64_t total = 0

    for i in range(n):
        total += a[i]
    return total 

def sum_float(ndarray[np.float64_t] a):
    cdef:
        Py_ssize_t i, n = len(a)
        np.float64_t total = 0

    for i in range(n):
        total += a[i]
    return total
时间安排 创建两个阵列,每个阵列包含100万个元素:

a_int = np.random.randint(0, 100, 10**6)
a_float = np.random.rand(10**6)

%timeit sum_int(a_int)
394 µs ± 30 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit a_int.sum()
490 µs ± 34.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit sum_float(a_float)
982 µs ± 10.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit a_float.sum()
383 µs ± 4.42 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
附加点
  • NumPy在浮动方面表现出色(相当大的幅度),甚至超过了它自己的整数和
  • sum_float
    的性能差异与缺少的
    boundscheck
    wrapparound
    指令相同。为什么?
  • sum\u int
    中的整数numpy数组转换为C指针(
    np.int64\u t*arr=a.data
    )可将性能提高25%。这样做对浮动没有任何作用
主要问题 如何在Cython中使用浮点数获得与使用整数相同的性能

编辑-只是计数很慢?!? 我编写了一个更简单的函数,它只计算迭代次数。前者将计数存储为int,后者存储为double

def count_int():
    cdef:
        Py_ssize_t i, n = 1000000
        int ct=0

    for i in range(n):
        ct += 1
    return ct

def count_double():
    cdef:
        Py_ssize_t i, n = 1000000
        double ct=0

    for i in range(n):
        ct += 1
    return ct
计算时间 我只运行了一次(害怕缓存)。不知道循环是否实际针对整数执行,但是
count\u double
的性能与上面的
sum\u float
相同。这太疯狂了

%timeit -n 1 -r 1 count_int()
1.1 µs ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

%timeit -n 1 -r 1 count_double()
971 µs ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

我不会回答你所有的问题,但只回答(在我看来)最有趣的问题

让我们从您的计数示例开始:

  • 编译器能够在整数情况下优化for循环-生成的二进制文件不计算任何内容-它只需返回编译阶段预先计算的值
  • 双精度情况并非如此,因为由于舍入错误,结果不会是
    1.0*10**6
    ,并且因为cython按照默认值以(非
    -ffast math
    )模式编译
  • 在查看cython代码时,您必须记住这一点:编译器不允许重新排列求和(IEEE 754),并且因为第一次求和的结果需要下一次求和,所以只有一条长线等待所有操作

    但最关键的一点是:numpy的功能与cython代码不同:

    >>> sum_float(a_float)-a_float.sum()
    2.9103830456733704e-08
    
    是的,没有人告诉numpy(与你的cython代码不同)总和必须这样计算

    ((((a_1+a2)+a3)+a4)+...
    
    numpy通过两种方式利用它:

  • 它执行(某种),从而导致较小的舍入误差

  • 它分块计算求和(python的代码有点难理解,下面是所用函数的列表
    pairwise\u sum\u DOUBLE

  • 第二点是您观察到的速度加快的原因,计算过程类似于以下模式(至少我从下面的源代码中了解到):

    这种求和的优点是:
    a2+a10
    的结果不依赖于
    a1+a9
    ,这两个值可以在现代CPU上同时计算(例如),这会导致您观察到的速度加快


    值得一提的是,在我的机器上,cython整数和比numpy的慢

    需要考虑numpy阵列的跨步(这仅在运行时已知,另请参见关于矢量化)会阻止某些优化。一种解决方法是使用内存视图,对于该视图,您可以明确数据是连续的,即:

    def sum_int_cont(np.int64_t[::1] a):
    
    这将导致我的机器显著加速(系数2):

    的确,在这种情况下,对double使用内存视图不会带来任何速度提升(不知道为什么),但总体而言,它简化了优化器的使用寿命。例如,将内存视图变量与
    -ffast math
    编译选项相结合,这将允许关联性,从而产生与numpy相当的性能:

    %%cython -c=-ffast-math
    cimport numpy as np
    def sum_float_cont(np.float64_t[::1] a):
        cdef:
            Py_ssize_t i, n = len(a)
            np.float64_t total = 0
    
        for i in range(n):
            total += a[i]
        return total
    
    现在:

    >>> %timeit sum_float(a_float)
    3.46 ms ± 226 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    >>> %timeit sum_float_cont(a_float)
    1.87 ms ± 44 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    >>> %timeit a_float.sum()
    1.41 ms ± 88.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    

    成对求和双的列表

    /*
    *两两求和,舍入误差O(lgn)而不是O(n)。
    *递归深度也是O(lgn)。
    *更新时也更新类似的复杂浮点求和
    */
    静态npy_双
    成对求和双倍(npy双倍*a,npy双倍,npy双倍)
    {
    if(n<8){
    npy_intp i;
    npy_双精度=0。;
    对于(i=0;i否则,如果(n)你知道如何从所有样板文件中提取Cython生成的C代码的相关部分吗?编译器可能会优化count_int(它最终是一个乘法),但不会优化double,因为+对于浮点算术来说不是关联的
    
    %%cython -c=-ffast-math
    cimport numpy as np
    def sum_float_cont(np.float64_t[::1] a):
        cdef:
            Py_ssize_t i, n = len(a)
            np.float64_t total = 0
    
        for i in range(n):
            total += a[i]
        return total
    
    >>> %timeit sum_float(a_float)
    3.46 ms ± 226 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    >>> %timeit sum_float_cont(a_float)
    1.87 ms ± 44 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    >>> %timeit a_float.sum()
    1.41 ms ± 88.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)