使用Cython优化Python函数以提高速度

12 投票
2 回答
9002 浏览
提问于 2025-04-16 11:04

几周前,我在网上问了一个关于如何提高用Python写的函数速度的问题。那时候,有人提到可以用Cython来实现这个目标。他还很友好地给了我一个示例,教我如何把那段代码用Cython处理。我想对下面的代码做同样的事情,看看通过声明变量类型能让它快多少。我有几个相关的问题。我看过cython.org上的教程,但还是有些疑问。这些问题是紧密相关的:

  1. 我对C语言一无所知。我需要学习哪些部分,才能用Cython来声明变量类型?
  2. 在C语言中,什么类型对应Python的列表和元组?比如,我可以在Cython中用double来表示Python中的float。那我该怎么处理列表呢?一般来说,在哪里可以找到某个Python类型对应的C类型?

如果能给我一个如何将下面的代码用Cython处理的示例,那就太好了。我在代码中插入了注释,说明了变量的类型。

class Some_class(object):
    ** Other attributes and functions **
    def update_awareness_status(self, this_var, timePd):
        '''Inputs: this_var (type: float)
           timePd (type: int)
           Output: None'''

        max_number = len(self.possibilities)
        # self.possibilities is a list of tuples.
        # Each tuple is a pair of person objects. 

        k = int(math.ceil(0.3 * max_number))
        actual_number = random.choice(range(k))
        chosen_possibilities = random.sample(self.possibilities, 
                                         actual_number)
        if len(chosen_possibilities) > 0:
            # chosen_possibilities is a list of tuples, each tuple is a pair
            # of person objects. I have included the code for the Person class
            # below.
            for p1,p2 in chosen_possibilities:

                # awareness_status is a tuple (float, int)
                if p1.awareness_status[1] < p2.awareness_status[1]:                   
                    if p1.value > p2.awareness_status[0]:
                        p1.awareness_status = (this_var, timePd)
                    else:
                        p1.awareness_status = p2.awareness_status
                elif p1.awareness_status[1] > p2.awareness_status[1]:
                    if p2.value > p1.awareness_status[0]:
                        p2.awareness_status = (price, timePd)
                    else:
                        p2.awareness_status = p1.awareness_status
                else:
                    pass     

class Person(object):                                         
    def __init__(self,id, value):
        self.value = value
        self.id = id
        self.max_val = 50000
        ## Initial awareness status.          
        self.awarenessStatus = (self.max_val, -1)

2 个回答

1

C语言并不直接支持列表这个概念。

它的基本数据类型包括 int(还有 charshortlong)、floatdouble(这些类型和Python中的对应类型比较简单明了),还有指针。

如果你对指针这个概念不太了解,可以看看这个链接:维基百科:指针

在某些情况下,指针可以用来替代元组或数组。字符指针是所有字符串的基础。

比如说你有一个整数数组,你会把它存储在一块连续的内存区域里,并且有一个起始地址。你需要定义类型(int)并且说明这是一个指针(*):

cdef int * array;

现在你可以这样访问数组中的每个元素:

array[0] = 1

不过,内存必须先分配(比如使用 malloc),而且高级索引是无法使用的(例如 array[-1] 会得到内存中的随机数据,超出分配空间的索引也是如此)。

更复杂的类型在C语言中并没有直接对应,但通常有一种C语言的方式可以做到这些,而不需要使用Python的类型(例如,for循环并不需要一个范围数组或迭代器)。

正如你自己注意到的,写出好的Cython代码需要对C语言有更详细的了解,所以接下来去找个教程学习是个不错的选择。

7

总的来说,你可以通过运行 cython 命令并加上 -a 这个“注释”选项,看到 Cython 为每一行源代码生成的 C 代码。具体的例子可以参考 Cython 的 文档。这在你想找出函数内部的瓶颈时,特别有帮助。

另外,还有一个叫做 “提前绑定以提高速度” 的概念,适用于将你的代码转换为 Cython。Python 对象(比如下面的 Person 类的实例)在访问属性时使用的是普通的 Python 代码,这在内层循环中会比较慢。我猜如果你把 Person 类改成 cdef class,你会看到一些速度上的提升。此外,你还需要在内层循环中给 p1p2 对象指定类型。

由于你的代码中有很多 Python 调用(比如 random.sample),除非你找到方法把这些行转换成 C 代码,否则你可能不会获得很大的速度提升,这需要花费不少精力。

你可以把东西写成 tuplelist,但这通常不会带来太大的速度提升。尽量在可能的情况下使用 C 数组;这部分你需要查一下。

通过下面这些简单的修改,我得到了 1.6 倍的速度提升。注意,我在这里和那里做了一些更改才能让它编译通过。

ctypedef int ITYPE_t

cdef class CyPerson:
    # These attributes are placed in the extension type's C-struct, so C-level
    # access is _much_ faster.
    cdef ITYPE_t value, id, max_val
    cdef tuple awareness_status

    def __init__(self, ITYPE_t id, ITYPE_t value):
        # The __init__ function is much the same as before.
        self.value = value
        self.id = id
        self.max_val = 50000
        ## Initial awareness status.          
        self.awareness_status = (self.max_val, -1)

NPERSONS = 10000

import math
import random

class Some_class(object):

    def __init__(self):
        ri = lambda: random.randint(0, 10)
        self.possibilities = [(CyPerson(ri(), ri()), CyPerson(ri(), ri())) for i in range(NPERSONS)]

    def update_awareness_status(self, this_var, timePd):
        '''Inputs: this_var (type: float)
           timePd (type: int)
           Output: None'''

        cdef CyPerson p1, p2
        price = 10

        max_number = len(self.possibilities)
        # self.possibilities is a list of tuples.
        # Each tuple is a pair of person objects. 

        k = int(math.ceil(0.3 * max_number))
        actual_number = random.choice(range(k))
        chosen_possibilities = random.sample(self.possibilities,
                                         actual_number)
        if len(chosen_possibilities) > 0:
            # chosen_possibilities is a list of tuples, each tuple is a pair
            # of person objects. I have included the code for the Person class
            # below.
            for persons in chosen_possibilities:
                p1, p2 = persons
                # awareness_status is a tuple (float, int)
                if p1.awareness_status[1] < p2.awareness_status[1]:
                    if p1.value > p2.awareness_status[0]:
                        p1.awareness_status = (this_var, timePd)
                    else:
                        p1.awareness_status = p2.awareness_status
                elif p1.awareness_status[1] > p2.awareness_status[1]:
                    if p2.value > p1.awareness_status[0]:
                        p2.awareness_status = (price, timePd)
                    else:
                        p2.awareness_status = p1.awareness_status

撰写回答