您的位置:首页 > 编程语言 > Python开发

Numpy学习笔记一、Why Numpy?

2017-11-29 14:20 627 查看

Numpy学习笔记一、Why Numpy?

前言

  Numpy是Python进行高性能数值计算的核心工具。我们普通的开发者只是需要了解Numpy的数据结构,会使用常规的切片和索引,调用封装好的函数,即可解决大部分工作场景。可为什么使用Numpy,因为其高效性,可以直接处理数组和矩阵,省略很多循环语句,那为什么Numpy的array那么快? Numpy是怎么做到的?

阅读目录:

一、Numpy是什么?

二、为什么选择Numpy?

三、 Numpy的向量化语句为什么比Python的for循环快?

一、 Numpy是什么?

  NumPy,既Numeric Python的缩写,是一个优秀的开源科学计算库,并已经成为Python科学计算生态系统的重要组成部分。Numpy为我们提供了丰富的数学函数、强大的多维数组对象以及优异的运算性能。使用Numpy就可以很自然的地使用数组和矩阵。

  尽管Python作为流行的编程语言非常灵活易用,但它本身并非为科学计算量身定做,在开发效率和执行效率上均不适合直接用于数据分析,尤其是大数据的分析和处理。幸运的是,Numpy为Python插上了翅膀,在保留Python语言优势的同时大大增强了科学计算和数据处理的能力。更重要的是,Numpy与Scipy、Matplotlib、Scikits等众多Python科学计算库很好结合在一起,共同构建了一个完整的科学计算生态系统,Numpy是使用Python进行数据分析的一个核心工具

二、 为什么选择Numpy?

  对于同样的数值计算任务,使用Numpy要直接编写Python代码便捷得多。这是因为Numpy能够直接对数组和矩阵进行操作,可以省略很多循环语句,其众多的数学函数会让编写代码的工作轻松很多。

  Numpy在数组操作上的效率优于纯Python代码,那么究竟快多少呢?下面是一段测试代码:

#2、测试Numpy在数组操作上的效率优于纯Python代码

#!/usr/bin/env/python
import sys
from datetime import datetime
import numpy as np

def pythonsum(n):
a = range(n)
b = range(n)
c = []

for i in range(len(a)):
a[i] = i ** 2
b[i] = i ** 3
c.append(a[i] + b[i])
return c

def numpysum(n):

a = np.arange(n) ** 2
b = np.arange(n) ** 3
c = a + b
return c

size = int(sys.argv[1])
start = datetime.now()
c = pythonsum(size)
delta = datetime.now() - start
print "The last 2 elements of the sum", c[-2:]
print "Pythonsum elapsed time inmicroseconds",delta.microseconds

start = datetime.now()
c = numpysum(size)
delta = datetime.now() - start
print "The last 2 elements of the sum", c[-2:]
print "Numpysum elapsed time in microseconds",delta.microseconds




  这里做了两组实验,第一组循环次数为2000,以微秒为基本单位,二者相差近10倍,当循环次数为10000,二者相差近25倍,这里可以直观看出,Numpy相对Python原生计算的
c7d2
效率提升。

三、 Numpy的向量化语句为什么比Python的for循环快?

  简单来说,numpy是C写的。为了方便初学者,如python之类语言在背后做了很多工作,这才能对外提供一个“傻瓜”界面。换句话说,“傻瓜”界面并非免费得来,在初学者看不到的地方,隐藏着巨大的、全方位的、不可避免的“消费陷阱”。

3.1、解释执行带来的开销

  举例来说,执行 x = 1234+5678 ,对编译型语言,是从内存读入两个short int到寄存器,然后读入加法指令,通知CPU内部的加法器动作,最后把加法器输出存储到x对应的内存单元(实质上,最后这个动作几乎总会被自动优化为“把加法器输出暂存到寄存器而不是内存单元,因为访问内存的时间消耗常常是访问寄存器的几十倍”)。一共2~4条指令(视不同CPU指令集而定)。

  而换了如python等解释性语言,先把“x = 1234+5678”当成字符串,逐个字符比对以分析语法结构——不计空格这也是11个字符,至少要做11个循环;每个循环至少需要执行的指令有:取数据(如读’x’这个字符)、比较数据、根据比较结果跳转(可能还得跳转回来)、累加循环计数器、检查循环计数器是否到达终值、根据比较结果跳转。这就是至少6条指令,其中包含一次内存读取、至少两次分支指令(现代CPU有分支预测,若命中无额外消耗,否则……)。总计66条指令,比编译型语言慢至少17倍(假设每条指令执行时间相同。但事实上,访存/跳转类指令消耗的时间常常是加法指令的十倍甚至百倍)。

  这还只是读入源码的消耗,尚未计入“语法分析”这个大头;加上后,起码指令数多数百倍。

3.2、字节码优化开销

  python比起其它解释性语言还是强很多的。因为它可以事先把文本代码编译成“字节码”(存储于扩展名为pyc的文件里),从而直接处理整型的“指令代码”,不再需要从头开始分析文本。但是,从“字节码”翻译到实际CPU代码这步,仍然是省不下的。

  这个消耗,可看作“利用虚拟机”执行异构CPU上的程序。有人证明过,哪怕优化到极致,这也需要10倍的性能消耗。这个消耗也有办法缩减。这就是JIT技术。JIT说白了,就是在第一遍执行一段代码前,先执行编译动作,然后执行编译后的代码。如果代码中没有循环,那么这将白白付出很多额外的时间代价;但若有一定规模以上的循环,就可能节省一点时间。

  这里面的佼佼者是Java。它甚至能根据上次运行结果实时profile,然后花大力气优化关键代码,从而得到比C更快的执行速度。不过,理想很丰满,现实很骨感。虽然局部热点的确可能更快,但Java的整体效率仍然比C/C++差上很多——这个原因就比较复杂了。和C/C++/Java那种投入海量资源经过千锤百炼的编译器不同,python的JIT甚至可称得上“蹩脚”。加加减减,仅一个循环,慢上十几甚至几十倍还是很正常的。以上讨论,仅仅考虑了for循环这个控制结构本身。事实上,“慢”往往是全方位的。

3.3、数据存储方式带来的开销

  要计算一组向量,首先就要存储它。

  对C/C++来说,就存在“数组”里;而它的数组,就是赤裸裸的一片连续内存区域;区域中每若干个字节就存储了一个数值数据。这种结构CPU处理起来最为方便快捷,且cache友好(若cache不友好就可能慢数倍甚至数十倍)。

  Java等其它语言就要稍逊一筹。因为它的“数组”是“真正的数组”;相对于“连续内存区域”,“真正的数组”就不得不在每次访问时检查数组下标有无越界。这个检查开销不大,但也不小……当然,这也是有好处的。至少不用像C/C++那样,整天担心缓冲区溢出了。

  而python之类,为了迁就初学者,它去掉了“变量声明”以及“数据类型”——于是它的用户再也用不着、也没法写 int xxx了。随便什么数据,咱想存就存,乌拉!

但是,如果我告诉你,可变数据类型其实在C/C++里面是这样声明的呢:

typedef struct tagVARIANT {
union {
struct __tagVARIANT {
VARTYPE vt;
WORD    wReserved1;
WORD    wReserved2;
WORD    wReserved3;
union {
LONGLONG            llVal;
LONG                lVal;
BYTE                bVal;
SHORT               iVal;
FLOAT               fltVal;
DOUBLE              dblVal;
VARIANT_BOOL        boolVal;
_VARIANT_BOOL       bool;
SCODE               scode;
CY                  cyVal;
DATE                date;
BSTR                bstrVal;
IUnknown            *punkVal;
IDispatch           *pdispVal;
SAFEARRAY           *parray;
BYTE                *pbVal;
SHORT               *piVal;
LONG                *plVal;
LONGLONG            *pllVal;
FLOAT               *pfltVal;
DOUBLE              *pdblVal;
VARIANT_BOOL        *pboolVal;
_VARIANT_BOOL       *pbool;
SCODE               *pscode;
CY                  *pcyVal;
DATE                *pdate;
BSTR                *pbstrVal;
IUnknown            **ppunkVal;
IDispatch           **ppdispVal;
SAFEARRAY           **pparray;
VARIANT             *pvarVal;
PVOID               byref;
CHAR                cVal;
USHORT              uiVal;
ULONG               ulVal;
ULONGLONG           ullVal;
INT                 intVal;
UINT                uintVal;
DECIMAL             *pdecVal;
CHAR                *pcVal;
USHORT              *puiVal;
ULONG               *pulVal;
ULONGLONG           *pullVal;
INT                 *pintVal;
UINT                *puintVal;
struct __tagBRECORD {
PVOID       pvRecord;
IRecordInfo *pRecInfo;
} __VARIANT_NAME_4;
} __VARIANT_NAME_3;
} __VARIANT_NAME_2;
DECIMAL             decVal;
} __VARIANT_NAME_1;
} VARIANT, *LPVARIANT, VARIANTARG, *LPVARIANTARG;


  简单说,这的思路就是“利用一个tag指示数据类型,真正的数据存储在下面的union里;访问时,依据tag指示转换/返回合适类型”。很显然,对C/C++/Java程序员来说,这无论时间还是空间上,都是个灾难。并且,它也极度的cache不友好——本来可以连续存储的,现在……变成了个结构体;而且一旦存了某些类型的数据,就不得不通过指针跳转到另一块区域才能访问(如果原地存储,浪费的空间就太恐怖了)。

3.4、小结

  哪怕仅仅了解到这个程度也已经很是触目惊心了:解释执行+字节码优化慢上至少10倍到几十上百倍,“初学者友好”的基础数据又慢上几倍到几十倍,透过容器访问(而非性能更好的、固定大小数组乃至不检查下标假装自己是数组的“内存区域”)再慢上几倍到几十倍……哪怕咱暂时不考虑其它机制带来的开销,仅把这几样往一块一凑(在某些特定的情况下,这些不同的“慢”点还可能相互影响、起到“迟缓度倍增放大”的效果)……

  除此之外,还有python内部如何管理/索引/访问脚本中的全局/局部变量的问题(一般会用dict)、用户数据和物理机存储器严重不匹配引起的缓存未命中问题、python内部状态机/执行现场管理等等方面管理的问题——对编译型语言,这些统统不存在,CPU/内存自己就把自己照顾的很好了;但对解释性语言,这些都会成为“迟缓度倍增”的元凶。

  这些东西的相互影响极为复杂微妙,几乎没人能彻底搞明白它。 请记住“在合适的地方用合适的工具”这句话——然后想办法搞明白每种工具的局限性吧。毕竟,哪怕是C/C++,在做矩阵之类运算时,也还会求助于SIMD的MMX指令、超线程/多核心CPU乃至GPU,以便为自己“增补”上并行处理能力呢。

很好奇Numpy底层实现,会在使用过程中,查资料陆续补充。

参考资料:

1、《Numpy学习指南2》

2、python的numpy向量化语句为什么会比for快?

https://www.zhihu.com/question/67652386/answer/255447886

3、对于 Python 的科学计算有哪些提高运算速度的技巧?

https://www.zhihu.com/question/67310504/answer/252179088?utm_medium=social&utm_source=wechat_session

4、VARIANT STRUCTURE

https://msdn.microsoft.com/en-us/library/windows/desktop/ms221627(v=vs.85).aspx
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: