Cython初窥

Posted NoneSec

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Cython初窥相关的知识,希望对你有一定的参考价值。

这篇关于Cython的文章主要是用来阐述什么是Cython,Cython的主要用途是什么。对于Cython的具体用法基本不涉及,因为我觉得了解它的主要用途以及它的优缺点,那么等到有使用场景的时候再来学习一下它的document就可以了。
1. Python的扩展模块(extention module)
我们知道可以用c、c++来扩展Python,这样做的目的就是为了把一些关键功能用更快、更高效的语言(c、c++)来实现,以提高Python程序的运行效率。
下面是一个示例:

#include<Python.h>

static PyObject *fun(PyObject *self, PyObject *args)
{
    int n, i, t = 12;

    if(!PyArg_ParseTuple(args, "i", &n))
    {
        return NULL;
    }
    for(i = 0; i < n; i++)
    {
        t = t + i;
    }

    return Py_BuildValue("i", t);
}


static PyMethodDef ForAddMethods[] = {
    {"fun",  fun, METH_VARARGS, "For loop add."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

PyMODINIT_FUNC
initforadd(void)
{
    (void) Py_InitModule("foradd", ForAddMethods);
}

这个扩展的函数非常简单,当然这个函数最后能比用Python来实现会快多少也难说,如果输入的n很大的话这个c语言版本可能会比纯Python版本要快不少了。
在用c语言作为Python的扩展的时候需要按照固定的格式来编写,还有我认为在用c扩展Python的时候更需要注意的是Python的引用计数问题,一旦对Python/C API不熟悉那么就很有可能会出现内存泄露的情况。在c代码中得程序自己来负责对象的引用管理,比如你调用PyInt_FromLong(12)创建了一个PyObject,那么你就需要记住在什么时候调用Py_XINCREFPy_DECREF来管理对象的引用。
2. 用Cython来生成Python的扩展
Cython是一个用来快速生成Python扩展模块(extention module)的工具,它的语法是Python语言语法和c语言语法的混血。
下面是一个用Python写的foradd功能:

def fun(n):
    t = 12
    i = 0
    while i < n:
        t = t + i
        i += 1
    return t

接着用cython -a test_foradd.pyx命令来生成一个.c和.html文件, 关于Cython的使用大家自行阅读文档吧,在本文中基本就只会用到这一条命令。
生成的test_foradd.c文件就是Cython把test_foradd.pyx”翻译”成的c语言版本,test_foradd.html是一个py代码和c代码对照的页面,可以在页面中看到每条py语句”翻译”成了哪几条c语句。也就是说你可以用Python来写一个需要c语言来实现的扩展模块,然后用Cython可以自动把Python”翻译”成c语言,这样你就无需关注前面我们自己动手用c语言来写Python扩展遇到的问题了。
生成的test_foradd.c的文件内容太多,我把关键的代码摘录如下:

static PyObject *__pyx_int_0;
static PyObject *__pyx_int_12;
static int __Pyx_InitGlobals(void) {
  __pyx_int_0 = PyInt_FromLong(0); 
  __pyx_int_12 = PyInt_FromLong(12);
  return 0;
}
/* Python wrapper */
static PyMethodDef __pyx_mdef_11test_foradd_1fun = {"fun", (PyCFunction)__pyx_pf_11test_foradd_fun, METH_O, 0};
PyMODINIT_FUNC PyInit_test_foradd(void)
{
  if (__Pyx_InitGlobals() < 0) __PYX_ERR(0, 1, __pyx_L1_error)
  __pyx_m = Py_InitModule4("test_foradd", __pyx_methods, 0, 0, PYTHON_API_VERSION); Py_XINCREF(__pyx_m);
}
static PyObject *__pyx_pf_11test_foradd_fun(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_n) {
  PyObject *__pyx_v_t = NULL;
  PyObject *__pyx_v_i = NULL;
  PyObject *__pyx_r = NULL;
  __Pyx_RefNannyDeclarations
  PyObject *__pyx_t_1 = NULL;
  int __pyx_t_2;
  __Pyx_RefNannySetupContext("fun", 0);

  /* "test_foradd.pyx":15
 * 
 * def fun(n):
 *     t = 12             # <<<<<<<<<<<<<<
 *     i = 0
 *     while i < n:
 */
  __Pyx_INCREF(__pyx_int_12);
  __pyx_v_t = __pyx_int_12;

  /* "test_foradd.pyx":16
 * def fun(n):
 *     t = 12
 *     i = 0             # <<<<<<<<<<<<<<
 *     while i < n:
 *         t = t + i
 */
  __Pyx_INCREF(__pyx_int_0);
  __pyx_v_i = __pyx_int_0;

  /* "test_foradd.pyx":17
 *     t = 12
 *     i = 0
 *     while i < n:             # <<<<<<<<<<<<<<
 *         t = t + i
 *         i += 1
 */
  while (1) {
    __pyx_t_1 = PyObject_RichCompare(__pyx_v_i, __pyx_v_n, Py_LT); __Pyx_XGOTREF(__pyx_t_1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 17, __pyx_L1_error)
    __pyx_t_2 = __Pyx_PyObject_IsTrue(__pyx_t_1); if (unlikely(__pyx_t_2 < 0)) __PYX_ERR(0, 17, __pyx_L1_error)
    __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
    if (!__pyx_t_2) break;

    /* "test_foradd.pyx":18
 *     i = 0
 *     while i < n:
 *         t = t + i             # <<<<<<<<<<<<<<
 *         i += 1
 *     return t
 */
    __pyx_t_1 = PyNumber_Add(__pyx_v_t, __pyx_v_i); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 18, __pyx_L1_error)
    __Pyx_GOTREF(__pyx_t_1);
    __Pyx_DECREF_SET(__pyx_v_t, __pyx_t_1);
    __pyx_t_1 = 0;

    /* "test_foradd.pyx":19
 *     while i < n:
 *         t = t + i
 *         i += 1             # <<<<<<<<<<<<<<
 *     return t
 */
    __pyx_t_1 = __Pyx_PyInt_AddObjC(__pyx_v_i, __pyx_int_1, 1, 1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 19, __pyx_L1_error)
    __Pyx_GOTREF(__pyx_t_1);
    __Pyx_DECREF_SET(__pyx_v_i, __pyx_t_1);
    __pyx_t_1 = 0;
  }

  /* "test_foradd.pyx":20
 *         t = t + i
 *         i += 1
 *     return t             # <<<<<<<<<<<<<<
 */
  __Pyx_XDECREF(__pyx_r);
  __Pyx_INCREF(__pyx_v_t);
  __pyx_r = __pyx_v_t;
  goto __pyx_L0;

  /* "test_foradd.pyx":14
 * # ChangeLog:
 * 
 * def fun(n):             # <<<<<<<<<<<<<<
 *     t = 12
 *     i = 0
 */

  /* function exit code */
  __pyx_L1_error:;
  __Pyx_XDECREF(__pyx_t_1);
  __Pyx_AddTraceback("test_foradd.fun", __pyx_clineno, __pyx_lineno, __pyx_filename);
  __pyx_r = NULL;
  __pyx_L0:;
  __Pyx_XDECREF(__pyx_v_t);
  __Pyx_XDECREF(__pyx_v_i);
  __Pyx_XGIVEREF(__pyx_r);
  __Pyx_RefNannyFinishContext();
  return __pyx_r;
}

虽然已经简化了不少代码,但上面的c代码还是看起来有点复杂,主要的原因就是里面的变量命名太随机了。这段c代码的功能跟我们前面自己手写的c扩展代码完成同一个功能,只是一个是”手工”一个是”自动”。
这里写图片描述
代码中的注释很好的解释了每一段代码分别代表了Python中的哪一行代码,还有如果不想看这么长的代码,那么可以直接打开生成的html文件,可以点击每一行带有+的语句,然后就会展示出来”翻译”后的c代码。页面中颜色越yellow的行表示最后”翻译”出来的c代码越多,也可以简单理解为这一行代码在Python中需要执行的opcode越多。
从这个示例来看Cython的”翻译”其实就是把Python代码”翻译”成了等价的c代码(其实也就是调用各种Python/C API),然后不再需要我们来关注c扩展程序的”格式”、各种PyObject对象的引用计数。Cython确实给我们写c扩展模块带来了不少的便利,即使你不懂Python/C API,甚至你可以不会c语言。
3. Dynamic Type(动态类型)和Static Type(静态类型)
C++、Java和C#都是静态语言,它们最大的特点就是变量在使用之前都必须进行类型声明。而Python、JS则是一种动态类型语言,所谓动态,通俗点说就是变量的类型是由最后赋予它的值决定的。
从运行效率来说静态类型是要优于动态类型的,因为在编译的时候就能确定每一个变量的类型,这样编译器就能对编译的结果做一些优化。而动态类型一般都是解释执行的,变量的类型需要在解释运行的时候才能确定,难免会损失一点性能。
对于如下的c代码:

int a = 1;
int b = 2;
int c = a + b;

假设对于+运算符有多种的实现版本(例如有整数版本、浮点数版本、),gcc编译器在编译的时候就知道a、b、c一定是整数类型,对于两个整数的相加那么编译器就可以选择整数版本作为编译的结果(虽然我不知道gcc到底有没有做),那么在运行的时候就省去了一个判断的过程。
相对的,对于如下的Python代码:

a = 1
b = 2
c = a + b

因为Python是动态类型,变量的类型只有在运行时才能确定其类型,上面的a + b这行代码在Python中执行时首先要判断a、b是否属于同一类型,如果不是因为Python属于强类型定义语言那么就会报TypeError: unsupported operand type(s) for +: 'int' and 'str'类似的错误。如果这两个变量是同一类型(整数),那么就会通过PyIntObject->PyObject_HEAD->ob_type获得对应整数类型的类型结构体PyInt_Type,然后再调用PyInt_Type->int_methods->int_add来进行两个整数的相加。这个过程是如此的”冗长”,有兴趣的同学可以看看PyNumber_Add这个函数的实现。
4. Cython的优势
用Cython自动生成的这个c语言扩展比我们手写的扩展代码要复杂很多(其实更多的是看起来复杂),手写几十行代码能搞定的最后Cython却生成了几百行代码,那么这个自动生成的扩展模块能带来性能上的提升吗?答案是:可能会带来性能的提升。
为什么会说可能呢?我们举例的这个是把纯Python代码用Cython来生成扩展模块,代码中没有引入任何Cython的语法,那么Cython就只能”照本宣科”的把每条Python语句”翻译”成对应的c语言版本,因为Python本身是用c语言来实现的而且Python/C API提供了丰富的接口,所以这种等价的”翻译”实现有了可能。
如果你有看过Python的源码那么你会发现Cython的”翻译”结果非常好理解,在Python中i=1这条Python语句就是调用PyInt_FromLong(1)来生成一个PyIntObject
我们这种的”照本宣科”的翻译其实跟纯Python代码在解释器中执行没有太大的区别,所以可能不会带来性能上的提升。那么Cython怎么样才能给我们带来性能的提升呢,答案见下一章节。
5. 进一步的优化
上面这个示例没有引入任何的Cython语法,所以最后带来的性能提升有限,那么我们再看一下进一步优化的版本:

def fun(n):
    cdef int t = 12
    cdef int i = 0
    while i < n:
        t = t + i
    return t

优化后的Python代码中引入了Cython的变量类型定义cdef int来定义一个整数变量,写到这里才发现前面提到的动态类型和静态类型貌似跟这篇文章没有太大的联系,但是我还是加上了,其实也可以理解Cython是混合了动态类型(Python)和静态类型(C语言),我们这次的优化就是把之前一些动态类型的变量变成静态类型的变量。
下面是Cython转换后的c代码:

static int __Pyx_InitGlobals(void) {
  if (__Pyx_InitStrings(__pyx_string_tab) < 0) __PYX_ERR(0, 1, __pyx_L1_error);
  return 0;
  __pyx_L1_error:;
  return -1;
}
static PyObject *__pyx_pf_12test_foradd2_fun(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_n) {
  int __pyx_v_t;
  int __pyx_v_i;
  PyObject *__pyx_r = NULL;
  __Pyx_RefNannyDeclarations
  PyObject *__pyx_t_1 = NULL;
  PyObject *__pyx_t_2 = NULL;
  int __pyx_t_3;
  __Pyx_RefNannySetupContext("fun", 0);

  /* "test_foradd2.pyx":15
 * 
 * def fun(n):
 *     cdef int t = 12             # <<<<<<<<<<<<<<
 *     cdef int i = 0
 *     while i < n:
 */
  __pyx_v_t = 12;

  /* "test_foradd2.pyx":16
 * def fun(n):
 *     cdef int t = 12
 *     cdef int i = 0             # <<<<<<<<<<<<<<
 *     while i < n:
 *         t = t + i
 */
  __pyx_v_i = 0;

  /* "test_foradd2.pyx":17
 *     cdef int t = 12
 *     cdef int i = 0
 *     while i < n:             # <<<<<<<<<<<<<<
 *         t = t + i
 *         i += 1
 */
  while (1) {
    __pyx_t_1 = __Pyx_PyInt_From_int(__pyx_v_i); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 17, __pyx_L1_error)
    __Pyx_GOTREF(__pyx_t_1);
    __pyx_t_2 = PyObject_RichCompare(__pyx_t_1, __pyx_v_n, Py_LT); __Pyx_XGOTREF(__pyx_t_2); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 17, __pyx_L1_error)
    __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
    __pyx_t_3 = __Pyx_PyObject_IsTrue(__pyx_t_2); if (unlikely(__pyx_t_3 < 0)) __PYX_ERR(0, 17, __pyx_L1_error)
    __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
    if (!__pyx_t_3) break;

    /* "test_foradd2.pyx":18
 *     cdef int i = 0
 *     while i < n:
 *         t = t + i             # <<<<<<<<<<<<<<
 *         i += 1
 *     return t
 */
    __pyx_v_t = (__pyx_v_t + __pyx_v_i);

    /* "test_foradd2.pyx":19
 *     while i < n:
 *         t = t + i
 *         i += 1             # <<<<<<<<<<<<<<
 *     return t
 */
    __pyx_v_i = (__pyx_v_i + 1);
  }

  /* "test_foradd2.pyx":20
 *         t = t + i
 *         i += 1
 *     return t             # <<<<<<<<<<<<<<
 */
  __Pyx_XDECREF(__pyx_r);
  __pyx_t_2 = __Pyx_PyInt_From_int(__pyx_v_t); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 20, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_2);
  __pyx_r = __pyx_t_2;
  __pyx_t_2 = 0;
  goto __pyx_L0;

  /* "test_foradd2.pyx":14
 * # ChangeLog:
 * 
 * def fun(n):             # <<<<<<<<<<<<<<
 *     cdef int t = 12
 *     cdef int i = 0
 */

  /* function exit code */
  __pyx_L1_error:;
  __Pyx_XDECREF(__pyx_t_1);
  __Pyx_XDECREF(__pyx_t_2);
  __Pyx_AddTraceback("test_foradd2.fun", __pyx_clineno, __pyx_lineno, __pyx_filename);
  __pyx_r = NULL;
  __pyx_L0:;
  __Pyx_XGIVEREF(__pyx_r);
  __Pyx_RefNannyFinishContext();
  return __pyx_r;
}

这里我只列出了跟章节3的代码有变化的部分,在Python中我们只加入了Cython的cdef int的类型定义,从最后”翻译”的结果来看效果非常明显,最后的c代码中不再出现PyNumber_Add这样的复杂函数,而是全部是简单的c代码。对于用cdef int修饰的变量i、t的所有操作都变成了我们常见的c操作,也无需在__Pyx_InitGlobals中用PyInt_FromLong来创建PyIntObject对象了。
6. 测试对比

def for_add(n):
    i = 0
    while True:
        if i > n:
            break
        i += 1
for_add(10000000)

对比结果如下:
Python:

real 0m0.938s
user 0m0.820s
sys 0m0.008s

Cython(cython test_foradd.pyx):

real 0m0.279s
user 0m0.268s
sys 0m0.008s

Cython(cdef int i):

real 0m0.269s
user 0m0.260s
sys 0m0.008s

从对比的接过来看直接用Cython的效果也还不错,比优化后的Cython没有差太多,从这个对比来看静态编译后确实比动态执行要有一定的优化效果。

以上是关于Cython初窥的主要内容,如果未能解决你的问题,请参考以下文章

Cython 代码分析

初窥项目构建

Swift初窥--使用Swift实现TableView

分析 Cython 代码时,啥是`stringsource`?

Cython保护python代码

初窥门径:从hello world开始rust学习