大神Bruce Eckel曾说:Life is short, you need Python!随后,Python创始人Guido就将印有“人生苦短,我用Python”的文化衫穿上身。
经过两位大师的调侃式互动,加之自身较为强大的脚本语言优势,Python近些年在大数据开发的常用高级语言中备受欢迎,大有超越Java的态势;尤其在数据科学、深度学习领域,Python是当仁不二的选择。
不过,当模型较复杂、运算量较大时,Python难免面临运算速度慢的尴尬。那么,选择Python就必须要忍受它的性能短板吗?
试试Cython吧!
Cython结合了Python的易用性和原生代码的速度,可以说是便捷性与高性能的完美融合。
接下来,就请本期大数据专家——凌逍跟大家分享他使用Cython加速Python计算的实践经验。
1
Python并行计算与GIL
提到Python的并行计算,不能不提极富争议性的GIL(global interpreter lock),全局解释器锁。为了利用多核CPU的计算资源,Python并行只能使用进程,如multiprocessing库等。如果用其他编程语言来写,那么开发者只需用把同步锁就可以把线程之间的通信过程协调好,而在Python中,我们却必须使用开销较高的multiprocessing模块。 multiprocessing的开销之所以比较大,原因就在于: 主进程和子进程之间,必须进行序列化和反序列化操作。对于某些较为独立,且数据量较小的任务来说,这套方案非常合适。所谓独立,是指待运行的函数不需要与程序中的其他部分共享状态。数据量较小是指主进程与子进程之间传递的数据比较小。如果待执行的运算不符合上述特征,那么multiprocessing所产生的开销,可能无法通过并行化来提升程序速度。在那种情况下,也可以利用multiprocessing所提供的些高级机制,如共享内存等。不过,这些特性用起来非常复杂而且效率很低,而且会有内存占用过多的问题。
下面通过一个简单例子来说明:
字典nodeDict中存放的是编码后的基站(key)与其他有切换关系的基站的列表(value)。我们想用与某个基站有切换关系的其他基站的经纬度来反向评估当前基站经纬度的异常。用最简单的方法,计算切换基站列表的平均经纬度和当前基站的距离。距离越大则表示当前基站定位可能出现异常。对于这种简单而量大计算,multiprocessing多进程加速的效率很低。
如下图所示:
可见大部分时间都浪费在进程之间的通信上和序列反序列化上,多进程的效率比单进程的还低。对于这种简单且量特别大的计算任务,多进程并行并不能加速任务。那么在Python的生态圈内有没有能使用线程来加速并行的神器呢?答案是有的,Cython从0.15版本开始就实现对原生并行编程的支持,使用OpenMP,让Pythoner可以轻松的绕过GIL的限制。
2
Cython中的并行
Cython可以直接理解为一种非常类似Python的语言,实际上Cython是一种部分包含C语言,以及完全包含Pyhton语言的一个超集。Cython和Python的一个显著区别就是,Cython的所有变量都可以明确声明变量类型。相同的Python程序仅仅是添加了类型声明就能在Cython中获得近50%的速度提升。
但真正让我们激动的是在Cython中可以无视GIL的存在而尽情使用线程加速,先来个简单演示:
对变量x做累加,看起来我们得到了一个非常错误的答案,并且每次运行代码时都会得到不同的答案,因为对x的写操作不是安全的。这就是需要锁的地方,在这种情况下,线程可以获取x上的锁,更新它,然后释放锁。这意味着一次只有一个线程在x上运行。将代码更改如下:
这样就能得到正确的结果。我们使用Cython来解决上面的问题,但不能再使用Python中的字典和列表,因为Python中的变量都自动带了锁(GIL)。还好Cython已经封装了C++标准库中的容器:deque,list,map,pair,queue,set,stack,vector。完全可以替代Python的dict, list, set等。
相互类型转化规则如下:
下面的函数将上述的基站切换字典转化成为了map类型,同理,计算距离的函数同样需要改为nogil形式:
Cython中有三种定义函数的方式,def, cdef以及cpdef。其中def对应为Python函数:如在Python中定义函数一样,以Python对象作为参数,返回Python对象。cdef为C函数,接受Python对象或C值作为参数,并且可以返回Python对象或C值,cdef函数不能直接在Python中调用。cpdef为混合函数,cpdef可以从任何地方调用,但是当从其他Cython代码调用时使用更快的C调用。即使从Cython调用,cpdef也可以被子类或实例属性上的Python方法覆盖。
如果发生这种情况,大多数性能优势会丢失。用Cython我们可以方便的向C代码传递和返回结果,Cython会自动为我们做相应的类型转化。
下面是我们定义的计算函数:
因为我们此次的数据量较小(约20万),为了展现并行计算的加速,我们仅仅对中间的计算部分计时,并且没有对结果进行收集。可以看到仅仅是单线程,Cython就将计算时间降至0.08秒左右,比Python版的程序速度提高了一百多倍。而且即使有更大的数据量,我们还可以开启多线程加速,加速效率接近于线性加速。
Cython通过对Python代码增加类型声明和直接调用C/C++函数,使得从Python代码中转成等价的C/C++代码的效率大大提高,而且它几乎支持全部Python特性,可以说任何Python代码都是有效的Cython代码,这使得引入Cython技术的成本降到了最低。目前很多Python库都使用Cython来提高效率,如pandas, scikit-learn, PyYAML等等。
领取专属 10元无门槛券
私享最新 技术干货