Cython架构
本节记录了spaCy的C级数据结构和接口,主要用于从Cython调用。部分属性主要供内部使用,所有C级函数和方法都优先考虑速度而非安全性——如果操作失误导致数组越界访问,程序可能会突然崩溃。
使用Cython时,有四种声明复杂数据类型的方式。遗憾的是我们在不同场景下会混合使用这四种方式,因为它们各有不同的用途:
| 声明 | 描述 | 示例 |
|---|---|---|
class | A normal Python class. | Language |
cdef class | A Python extension type. Differs from a normal Python class in that its attributes can be defined on the underlying struct. Can have C-level objects as attributes (notably structs and pointers), and can have methods which have C-level objects as arguments or return types. | Lexeme |
cdef struct | A struct is just a collection of variables, sort of like a named tuple, except the memory is contiguous. Structs can’t have methods, only attributes. | LexemeC |
cdef cppclass | A C++ class. Like a struct, this can be allocated on the stack, but can have methods, a constructor and a destructor. Differs from cdef class in that it can be created and destroyed without acquiring the Python global interpreter lock. This style is the most obscure. | StateC |
spaCy中最重要的类被定义为cdef class对象。这些对象的基础数据通常会被收集到一个结构体中,该结构体通常命名为c。例如,Lexeme类持有一个LexemeC结构体,位于Lexeme.c。这使您可以摆脱Python容器,并将指向基础数据的指针传递给C级函数。
Conventions
spaCy的核心数据结构被实现为Cython
cdef类。内存管理通过
cymem cymem.Pool类实现,它允许
你分配内存,这些内存在Pool对象被垃圾回收时
会自动释放。这意味着你通常不必担心内存释放问题。
你只需要决定哪个Python对象拥有该内存,并让它拥有
Pool。当该对象超出作用域时,内存就会被释放。你确实
需要注意不要让指针比拥有它们的对象存活更久——但这
通常相当容易做到。
所有Cython模块都应在文件顶部添加# cython: infer_types=True编译器指令。这能使代码更加简洁,避免了许多类型声明的需要。如果可能的话,即使您不太关心多线程,也应该优先将函数声明为nogil。原因是nogil函数能极大地帮助Cython编译器理解您的代码——您是在告诉编译器这里不可能出现Python动态特性。这能让许多错误被提前发现,并确保您的函数能以C语言的速度运行。
Cython提供了多种序列选择:可以是Python列表、numpy数组、内存视图、C++向量或指针。指针是首选方案,因为它们速度最快、语义最明确,并能让编译器更严格地检查代码。C++向量也很出色——但应仅限于在函数内部使用。将向量作为参数接收就不太友好,因为这要求用户做更多工作。以下是获取numpy数组、内存视图或向量指针的方法:
C数组和C++向量都向编译器保证,不会对变量执行Python操作。这是一个很大的优势:它让Cython编译器能为你捕获更多错误。
从NumPy数组或memoryview获取指针时,请注意数据实际上是以C连续顺序存储的——否则你将得到一个指向无效数据的指针。如果传入内存布局不正确的缓冲区,上述代码中的类型声明应会生成运行时错误。要遍历数组,推荐使用以下风格:
如果这令人困惑,请考虑到编译器无法处理
for item in int_array: ——原始指针没有附带长度信息,那么我们如何
确定在哪里停止?切片表示法提供了长度作为这个问题的解决方案。
注意,在上面的代码中我们不必声明item的类型——
编译器可以轻松推断出来。这让我们获得了看起来很像Python、
但速度与C完全相同的简洁代码——因为我们确保了
编译到C的过程非常简单。
Your functions cannot be declared nogil if they need to create Python objects
or call Python functions. This is perfectly okay — you shouldn’t torture your
code just to get nogil functions. However, if your function isn’t nogil, you
should compile your module with cython -a --cplus my_module.pyx and open the
resulting my_module.html file in a browser. This will let you see how Cython
is compiling your code. Calls into the Python run-time will be in bright yellow.
This lets you easily see whether Cython is able to correctly type your code, or
whether there are unexpected problems.
一旦克服了最初的学习曲线,使用Cython工作会带来丰厚的回报。与C和C++类似,在Cython中首次编写的代码通常就是性能最优的方案。相比之下,Python优化往往需要大量试错:用if item in my_dict判断更快,还是用.get()更快?try/except呢?这个numpy操作会创建副本吗?这些问题根本无法靠猜测得到答案,优化结果也往往难以令人满意——因此你永远不知道何时该停止优化。最糟糕的情况下,你会把代码搞得一团糟,引诱下一位阅读者继续碰运气。这就像那些火山毒气陷阱,救援者接连因缺氧昏厥,导致更多救援者前赴后继地倒下。简而言之,请直接拒绝优化Python代码。如果第一次运行速度不够快,直接换用Cython吧。