This page looks best with JavaScript enabled

Cython混合编程(2)

 ·  ☕ 5 min read · 👀... views

0x00 Cython静态类型再提速

在上一节中,已经介绍了将Python代码通过Cython编译为低级的机器码作为库来提速的方法,但实际上,上一节的代码中,pyx文件里用到的函数与变量依旧属于python的动态类型。Python的动态类型可以使变量关联到不同类型的对象,正是这个特点使Python灵活而动态,但正是这个特点也会给解释器带来大量的负担,因为解释器必须在运行的过程中动态的类型与其包含的方法。Cython是Python语言的一个超类,它支持显示的类型声明,可使编译出的拓展在极大程度上再优化。

0x01 变量

在Cython中,声明变量类型可再其前面加 cdef <type> 的字样,从而定义类型化变量,例如

1
2
3
4
5
cdef int i   # 声明一个16位整数变量
cdef double a, b = 2.1, c = 3.4    #同时声明多个变量,并可进行初始化
cdef object p    #将变量声明为object类型,可将任何类型的对象赋给它,但对性能的提升毫无帮助

a = <double> i   # 将int类型的变量i强制转化为double类型,并赋值给a

在Python中,变量被认为是指向内存中对象的标签,一切都以对象的形式存储。但是类型化变量的行为截然不同,它只能将适合的值存储到容器中,是否适合取决于容器的数据类型,若出错则会抛出异常(与C语言类似)。

静态类型的优化是巨大的。例如,如果将一个循环索引声明为 int 类型,Cython将使用纯粹的C代码重写循环,使其不依赖于Python解释器,该变量在运行期间将始终为int类型,编译器会自动的对其进行优化。下面将用一个测试来演示这个优化效果有多显著。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# example.pyx
def example():
    sum = 0
    for i in range(1000):
        sum += i
    return sum


def static_example():
    cdef int i, sum = 0
    for i in range(1000):
        sum += i
    return sum
1
2
3
4
5
6
7
# setup.py
from distutils.core import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize("example.pyx")
)
1
2
3
4
5
6
7
# t.py
import timeit
print(timeit.timeit(stmt="example.example()",setup="import example",number=1000))
print(timeit.timeit(stmt="example.static_example()",setup="import example",number=1000))

# 0.046184100000000006
# 0.00026140000000000885

可以看到,使用静态变量,使代码运行速度快了两百多倍!由此可见,将依赖于运行缓慢的解释器的Python代码编译为高效机器码对性能提升的重要性

0x02 函数

同样的,对于函数,我们也可以将其定义为静态类型。在参数名前指定类型,将会对参数进行类型检查,使参数成为类型化变量。

1
2
def maxnum_python(int a, int b):
    return a if a>b else b

在上面的函数中,虽然将参数定义为类型化变量,但是函数依旧是python函数,调用时依旧需要切换到解释器。我们可以通过将函数的返回值也定义为静态类型,使函数转化为原生的C语言函数

1
2
cdef int maxnum_c(int a, int b):
    return a if a>b else b

虽然这种方式将Python函数转化为原生C语言函数,可极大降低开销,但是也带来一个重大缺陷。这种函数不对Python可见,也就是我们无法在Python中调用它们,它们的作用域仅为当前的Cython文件。但是可以通过写共享声明解决这一问题。还有一个方法就是生成这个函数的两个版本,一个用于解释器使用的Python版本,一个用于在Cython中使用的快速C语言函数。Cython已经帮我们集成了这一功能,只需要将cdef关键字更换为cpdef即可,其余用法均相同。

1
2
cpdef int maxnum_hybrid(int a, int b):
    return a if a>b else b

函数内联

虽说C语言函数相较于Python函数,已经极大降低了开销,但实际上调用产生的开销还是不少的,如果被调用多次这个开销会更加显著。内联函数会建议编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支,是一种特别的用于消除调用函数时所造成的固有的时间消耗方法。但同样的,这个方法实际上是在程序占用空间和程序执行效率之间进行权衡,因为过多的比较复杂的函数进行内联扩展将带来很大的存储资源开支。因此对于函数体很短的函数,非常建议采用这种内联的做法,只需在函数定义前加上关键字 inline 即可。

1
2
cdef inline int maxnum_c(int a, int b):
    return a if a>b else b

0x03 类

同样的,类也是可以用cdef关键字声明为静态类型,比如这个类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# example.pyx
cdef class Point:
    cdef double x            # Python不可直接访问
    cdef public double y     # Python可直接访问
    cdef readonly double tmp # Python只读
    
    def __init__(self, double x, double y):
        self.x = x
        self.y = y
        self.tmp = x*y

    cpdef double norm(self):
        return (self.x **2 + self.y **2)**0.5

# t.py
import example
a = example.Point(1.1, 2.2)
print(a.norm())

与函数不同,直接的将类用cdef关键字定义为静态类型,在Python中也可直接创建其实例。这个Point类用于存储坐标并且类方法可计算该坐标到原地的距离。x,y在类中被声明为静态类型的类变量,在类方法中访问这样被声明类型的属性,Cython将绕过开销很大的属性查找,直接访问底层C结构体中的指定字段,通过这种方法,可使得访问类型化类的属性速度远远快于Python类。

但是同样的,cdef class也有几个缺点。首先,cdef class中使用的变量,必须显示的声明它们的类型,比如这个例子中声明类变量 x,y,tmp,若不进行声明,将抛出AttributeError异常。然后便是上面的注释中所说的,如果不对属性进行特殊声明,在Python中同样不可直接访问,会抛出AttributeError异常

这个例子中的类方法norm可计算该坐标到原点的距离,这里我使用的cpdef关键字使该方法可在Python中调用,若使用cdef方法,也会像函数一样在Python中不可见。但还看到了有些dalao将类型化类派生出一个Python类,并使用纯粹的Python拓展其属性和方法。这里对这种骚操作先不做介绍(博主tcl…也不会,Orzz)

0x04 参考资料

  1. https://cython.org/
  2. https://baike.baidu.com/item/%E5%86%85%E8%81%94%E5%87%BD%E6%95%B0/9567625?fr=aladdin
  3. 《Python高性能(第二版 )》
Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer