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对象了。

有心的同学可以测试一下这两个之间的性能提升。

时间: 2024-10-25 12:26:22

Cython初窥的相关文章

Scrapy 1.4 文档 01 初窥 Scrapy

初窥 Scrapy Scrapy 是用于抓取网站并提取结构化数据的应用程序框架,其应用非常广泛,如数据挖掘,信息处理或历史存档. 尽管 Scrapy 最初设计用于网络数据采集(web scraping),但它也可用于使用 API(如 Amazon Associates Web Services)提取数据或用作通用的网络爬虫. 爬虫(spider)示例 为了向您展示 Scrapy 带给您的是什么,我们将使用最简单的方式运行一个爬虫,向您展示一个 Scrape Spider 的例子. 这是一个爬虫的

jQuery源码学习(2):选择器初窥

选择器初窥 代码架构: jQuery选择器可以依照传入数据的类型分为五大类: 传入字符串:$("div"), $("#id"), $(".div1"),$(".div p.title") 传入html代码:$("<div></div>"), $("<div>1</div><div>2</div>") 传入对象:$(d

Boost.ASIO简要分析-1 初窥

Boost.Asio是一个主要用于网络及底层I/O编程的跨平台C++库. 1. 初窥 Boost.Asio支持对I/O对象进行同步及异步操作. 1.1 同步操作 同步操作的事件顺序如下图所示: 1) 调用者调用I/O对象的connect函数开始连接操作,socket.connect(server_endpoint): 2) I/O对象将连接请求传递给io_service: 3) io_service调用操作系统函数: 4) 操作系统返回结果给io_service: 5) io_service将结

Swift初窥----深入Swift

存储函数 内存中的Fibonacci函数,避免重复递归,来提高代码执行效率 模板 编译器 Swift编译器,使其可以编译出更快的机器代码 Swift初窥----深入Swift,布布扣,bubuko.com

初窥netfilter/iptables

做这个东西太麻烦了,一不小心,就被自己关门外了. ---------------------------------------------- 一.前言 二.环境 三.语法解析 四.配置及测试 1.SNAT案例 2.DNAT案例 3.SSH案例 4.SSH深入案例(自定义规则) 5.web和ftp(自定义规则) 6.web和ftp(系统默认规则) 五.保存 ---------------------------------------------- 一.前言 iptables即Linux 内核集

初窥ElasticSearch

初窥ElasticSearch 官网上面的,不知道讲的是什么.. youtube上面有一个start with,内容是在windows下面跑这个elastic search,然后用一个fidler工具可视化测试 https://www.youtube.com/watch?v=60UsHHsKyN4 粗略看起来,其实es和其他db没什么大区别,只是在搜索上有很多强大功能,所以很适合用在需要搜索的项目.貌似用curl发送一个JSON格式的数据(实际上是命令)到es就可以做CRUD elasticse

C++拾遗(二)——初窥标准库类型

本篇博文的开始,先介绍一道书上看到的智力题:有20瓶药丸,其中19瓶装有1克/粒的药丸,余下一瓶装有1.1克/粒的药丸.有一台称重精准的天平,只是用一次天平的情况下如何找出比较重的那瓶药丸? 好了,直接公布答案.从药瓶#1取出一粒药丸,从药瓶#2取出两粒,从药瓶#3取出三粒,依此类推.如果每粒药丸均重1克,则称得总重量为210克(1 + 2 + … + 20 = 20 * 21 / 2 = 210),“多出来的”重量必定来自每粒多0.1克的药丸.药瓶的编号可由算式(weight - 210 gr

初窥CSS布局(一篇文章学会布局)

写一篇文章,难免要为之命名,所谓名不正,则言不顺:言不顺,则事不成.这篇文章是要说明一下CSS中的布局,实为入门之法矣. 本想命名为"布局说"的,但是总感觉题目太大,被大神们看到难免沦为笑柄,思来想去,便命名为"初窥CSS布局". 不管是写一个html页面,还是打算建一个网站,首先应该想的是怎么为之布局,这是常常让我头疼的事情,不知,这是否也曾困扰着 足下?您是怎么为页面布局的呢?是否有为页面布局的通用之法呢?我不知道.但是下文,就是从头到尾介绍了一种页面布局的方法

初窥c++11:lambda函数

为什么需要lambda函数 匿名函数是许多编程语言都支持的概念,有函数体,没有函数名.1958年,lisp首先采用匿名函数,匿名函数最常用的是作为回调函数的值.正因为有这样的需求,c++引入了lambda 函数,你可以在你的源码中内联一个lambda函数,这就使得创建快速的,一次性的函数变得简单了.例如,你可以把lambda函数可在参数中传递给std::sort函数 #include <algorithm> #include <cmath> void abssort(float*