使用 f2py、Cython 和 Numba 进行循环优化

计算科学 Python 正则 表现
2021-12-22 18:58:46

我尝试使用 f2py、Cython 和 Numba 使简单的循环结构在 python 中更快。Python实现:

def Python(A):
    B = 0.0
    for i in range(100):
        for j in range(100):
            for k in range(100):
                for l in range(100):
                    B = B + A[i,j,k,l]
return B

Numa 实现:

@jit(double(double [:,:,:,:]), nopython=True)
def Numba(A):
    B = 0.0
    for i in range(100):
        for j in range(100):
            for k in range(100):
                for l in range(100):
                    B = B + A[i,j,k,l]
return B

Cython 实现:

cdef double Cython(double [:,:,:,:] A):
    cdef double B
    cdef int i, j, k, l
    B = 0.0
    for i in range(0,100):
        for j in range(0,100):
            for k in range(0,100):
                for l in range(0,100):
                    B = B + A[i,j,k,l]
return B

f2py 实现:

SUBROUTINE F2PY_run(A,B)
    INTEGER :: i, j, k, l
    REAL(8), DIMENSION(100,100,100,100), INTENT(in) :: A
    REAL(8), INTENT(out) :: B

    B = 0.0d0
    DO i=1, 100
        DO j=1, 100
            DO k=1, 100
                DO l=1,100
                   B = B + A(l,k,j,i)
                END DO
            END DO
        END DO
    END DO
END SUBROUTINE F2PY_run

Cython 编译:

setup(ext_modules=cythonize(Extension('runtest',['runtest.pyx'])), include_dirs=[numpy.get_include()])

f2py 编译:

f2py -c runFortran.F90 --f90flags=-O3 -m fortranrun

这些函数是通过将 np.random.rnd(100,100,100,100) 传递给函数来运行的。时间安排如下:

22.998 sec; Python 
 0.114 sec; Cython 
 2.678 sec; f2py   
 0.106 sec; numba  

我知道对于我设置的这个特定问题,我可以使用 np.einsum()。np.einsum() 的时间为 0.061 秒。但是对于我的“真实”代码,einsum 无法解决这个问题。可以看出 Cython 和 Numba 的执行速度大致相同,而 f2py 则慢得多。f2py 函数是用 Fortran90 编写的,所以我原以为它至少会这么快。因此,我的问题是,我是否错误地使用了 f2py?还是 f2py 有速度问题?

更新 1 根据 Stefano M 的建议,我做了一份 F2PY 性能报告。

                  /-----------------------\ 
                 < F2PY performance report >  
                  \-----------------------/ 
Overall time spent in ...
(a) wrapped (Fortran/C) functions           :      105 msec 
(b) f2py interface,                1 calls  :     2925 msec
(c) call-back (Python) functions            :        0 msec
(d) f2py call-back interface,      0 calls  :        0 msec
(e) wrapped (Fortran/C) functions (acctual) :      105 msec

可以看出几乎所有时间都在界面中使用!下面是如何调用函数:

import numpy as np
import time
import fortranrun

A = np.random.rand(100,100,100,100)

start = time.time()
B = fortranrun.f2py_run(A)
print(time.time()-start, B, 'f2py')
1个回答

我认为问题与 f2py 生成 fortran 接口的方式有关:参数 tofortranrun.f2py应存储为 F_CONTIGUOUS 数组,否则接口将创建具有正确存储顺序的内部副本。

Python 3.6.2 (default, Jul 22 2017, 21:19:22) 
[GCC 7.1.1 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy as np
>>> import fortranrun
>>> A = np.random.rand(100,100,100,100)
>>> A.flags
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  UPDATEIFCOPY : False
>>> A.T.flags
  C_CONTIGUOUS : False
  F_CONTIGUOUS : True
  OWNDATA : False
  WRITEABLE : True
  ALIGNED : True
  UPDATEIFCOPY : False
>>> b = fortranrun.f2py(A.T)
>>> 
                      /-----------------------\
                     < F2PY performance report >
                      \-----------------------/
Overall time spent in ...
(a) wrapped (Fortran/C) functions           :      761 msec
(b) f2py interface,                1 calls  :        0 msec
(c) call-back (Python) functions            :        0 msec
(d) f2py call-back interface,      0 calls  :        0 msec
(e) wrapped (Fortran/C) functions (acctual) :      761 msec

这里你看到的A是 C_CONTIGUOUS,而转置A.T是 F_CONTIGUOUS,调用b = fortranrun.f2py(A.T)没有 f2py 接口开销。

相反,当你调用 时b = fortranrun.f2py(A),由于A不是 F_CONTIGUOUS,f2py 接口会分配暂存内存并A按照正确的存储顺序复制原始数组。

内存顺序是 C-FORTRAN 接口的一个常见主题:您应该构造A为 F_CONTIGUOUS 数组,或者传递A.T给您的 fortran 例程,但请记住您正在对转置数组进行操作。

可以使用编译选项生成有关数组参数副本的警告

-DF2PY_REPORT_ON_ARRAY_COPY=1

另请参见numpy 数组的内部组织