Cython性能基准测试

时间:2016-02-11 18:41:48

标签: python performance benchmarking cython

我正在尝试获得针对cython和numpy的FLOPS基准测试。为此,我在cython中编写了一个程序。这是:

cimport numpy as np
import numpy as np
import time

cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
def numpybenchmark():

    cdef np.ndarray[np.float64_t, ndim=2] m1 = np.random.rand(3,3)
    cdef np.ndarray[np.float64_t, ndim=1] m2 = np.random.rand(3)
    cdef np.ndarray[np.float64_t, ndim=1] res

    cdef int niters = 10000000
    cdef int x

    t1 = time.time()
    for x in range(niters):
        res = np.dot(m1, m2)
    t2 = time.time()
    cdef double numopsperloop = 9. + 6.
    cdef double totalops = numopsperloop * float(niters)
    cdef double mflops = totalops / (t2-t1) / 1024. / 1024.
    print 'Computed MFLops is: ' + str(mflops)

在我的机器上,我测量“Computed MFLops is:7.42390102416”。我的机器配备Intel Core i7-6700HQ CPU @ 2.6 GHz并运行Windows 10。

如果要在计算机上运行它,请将代码保存在名为“benchmark.pyx”的文件中。然后创建一个名为“setup.py”的文件,其中包含以下内容:

from distutils.core import setup                                                                 
from Cython.Build import cythonize                                                               
import numpy                                                                                     

setup(                                                                                           
    ext_modules = cythonize("benchmark.pyx"), 
    include_dirs=[numpy.get_include()]                                                           
)  

然后你应该能够用“python setup.py build_ext --inplace”编译它。在Windows上可能会有点困难,因为我遇到了可怕的“无法找到vcvarsall.bat”错误,并且不得不花费大量精力来解决这个问题。

这种表现对我来说似乎很糟糕。我想知道是否有人可以在他们的平台上运行它并告诉我你得到了什么?或指出我的代码中对性能产生负面影响的任何明显错误?

谢谢!

2 个答案:

答案 0 :(得分:2)

Cython实际上并没有消除np.dot上的任何Python调用开销。这涉及(请注意,该列表并非详尽无遗,并且在某些地方可能略有错误,但它给出了要点):

  1. 查找np.dot来致电:

    • np
    • 的全局命名空间中的字典查找
    • np dot命名空间中的词典查找。 (注意,通过在函数内部执行dot = np.dot,然后调用dot
    • ,可以消除上述所有问题。
    • dot __call__上的字典查找。 (如果dot是C / Fortran编译函数,可以通过更快的机制完成)
  2. 打包准备np.dot的参数:

    • 创建一个包含传递给np.dot
    • 的两个参数的元组
    • 增加每个参数的引用计数。
  3. np.dot然后处理参数......

    • 解包元组
    • 检查numpy数组中的每个参数。
    • 检查每个numpy数组的dtype是否相同,并根据BLAS函数调用的dtype选项进行检查。
    • 检查数组维度并确保它们匹配。
  4. ...为输出参数分配空间......

    • 分配新的np.ndarray对象
    • 增加
    • 的引用计数
    • ndarray
    • 中为物理阵列分配空间
  5. ...调用BLAS操作,为您提供浮点操作...

  6. ...并减少传递的输入参数的引用计数(检查是否应该释放任何参数,但不会有任何参数)

  7. 您的通话功能必须:

    • 检查np.dot
    • 是否引发了异常
    • 接收输出数组(可能是一些refcount在这里玩杂耍)
    • 减少res
    • 之前内容的引用计数
    • 释放res的先前内容,记住它至少是一个两步过程,因为该数组与ndarray持有者分开存放。
  8. 如果你想使大部分(除了可能的分配)与矩阵向量乘法相比无关紧要,那么你需要对明显更大的数组进行测量。您可以使用out中的np.dot可选参数来摆脱分配。如果你想让它全部消失,那么你可以使用scipy Cython BLAS interface直接调用BLAS函数。

答案 1 :(得分:1)

在仔细阅读DavidW的帖子并做了一些实验之后,我找到了一种方法来避免所有笨拙的开销。它涉及使用指针,特别是不将numpy数组传递给循环内的函数。

以下是完整代码:

cimport numpy as np
import numpy as np
import time


cdef matrixdotvector(double* mat, int numrows, int numcols, double* vec, double* outputvec):
    outputvec[0] = mat[0+0*numcols] * vec[0] + mat[1+0*numcols] * vec[1] + mat[2+0*numcols] * vec[2]
    outputvec[1] = mat[0+1*numcols] * vec[0] + mat[1+1*numcols] * vec[1] + mat[2+1*numcols] * vec[2]
    outputvec[2] = mat[0+2*numcols] * vec[0] + mat[1+2*numcols] * vec[1] + mat[2+2*numcols] * vec[2]

cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
def numpybenchmark():

    cdef np.ndarray[np.float64_t, ndim=2] m1 = np.random.rand(3,3)
    cdef np.ndarray[np.float64_t, ndim=1] m2 = np.transpose(np.random.rand(3))
    cdef np.ndarray[np.float64_t, ndim=1] res

    cdef int niters = 10000000
    cdef int x

    t1 = time.time()
    for x in range(niters):
        res = np.dot(m1, m2)
    t2 = time.time()
    cdef double numopsperloop = 9. + 6.
    cdef double totalops = numopsperloop * float(niters)
    cdef double mflops = totalops / (t2-t1) / 1024. / 1024.
    print 'Computed MFLops is: ' + str(mflops)

cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
def numpybenchmark2():

    cdef int numrows = 3
    cdef int numcols = 3
    cdef np.ndarray[np.float64_t, ndim=2] m1 = np.random.rand(3,3)
    cdef np.ndarray[np.float64_t, ndim=1] m2 = np.transpose(np.random.rand(3))
    cdef np.ndarray[np.float64_t, ndim=1] res = np.zeros(3)

    cdef int niters = 10000000
    cdef int x

    t1 = time.time()
    for x in range(niters):
        matrixdotvector(&m1[0,0], numrows, numcols, &m2[0], &res[0])
    t2 = time.time()

    assert (np.linalg.norm(np.dot(m1,m2) - res) < 1.0e-6), "Arrays do not match"

    cdef double numopsperloop = 9. + 6.
    cdef double totalops = numopsperloop * float(niters)
    cdef double mflops = totalops / (t2-t1) / 1024. / 1024.
    print 'Computed MFLops is: ' + str(mflops)

numpybenchmark()和numpybenchmark2()之间的最大区别在于我通过将指针传递给numpybenchmark2()中的numpy数据数组来避免所有numpy开销(而不是传递类型化的numpy对象,这也很慢) 。我通过展开它并在代码中重新实现它来避免np.dot计算的开销。

所以我现在得到的基准测试结果是:

在[13]中:benchmark.numpybenchmark() 计算的MFLops是:7.3412268815

在[14]中:benchmark.numpybenchmark2() 计算的MFLops是:1521.81908107

所以这是一个相当大的增长。老实说,这不是一种“pythonic”方式,但它快速尖叫,所以在某些情况下可能有用。由于matrixdotvector()中的代码看起来像C代码,因此可以认为这应该全部用C语言编写。就个人而言,我发现使用类似C语言的cython代码实现原型更快,而不是直接进入C语言。

无论如何,也许这篇文章对某些正在学习cython的人来说是有用的。