关于python:python的numpy向量化语句为什么会比for快

54次阅读

共计 3619 个字符,预计需要花费 10 分钟才能阅读完成。

咱们先来看看,python 之类语言的 for 循环,和其它语言相比,额定付出了什么。

咱们晓得,python 是解释执行的。

举例来说,执行 x = 1234+5678,对编译型语言,是从内存读入两个 short int 到寄存器,而后读入加法指令,告诉 CPU 外部的加法器动作,最初把加法器输入存储到 x 对应的内存单元(本质上,最初这个动作简直总会被主动优化为“把加法器输入暂存到寄存器而不是内存单元,因为拜访内存的工夫耗费经常是拜访寄存器的几十倍”)。一共 2~4 条指令(视不同 CPU 指令集而定)。

换了解释性语言,状况就大大不同了。

它得先把“x = 1234+5678”当成字符串,一一字符比对以剖析语法结构——不计空格这也是 11 个字符,至多要做 11 个循环;每个循环至多须要执行的指令有:取数据(如读 ’x’ 这个字符)、比拟数据、依据比拟后果跳转(可能还得跳转回来)、累加循环计数器、查看循环计数器是否达到终值、依据比拟后果跳转。这就是至多 6 条指令,其中蕴含一次内存读取、至多两次分支指令(古代 CPU 有分支预测,若命中无额定耗费,否则……)。总计 66 条指令,比编译型语言慢至多 17 倍(假如每条指令执行工夫雷同。但事实上,访存 / 跳转类指令耗费的工夫经常是加法指令的十倍甚至百倍)。

这还只是读入源码的耗费,尚未计入“语法分析”这个大头;加上后,起码指令数少数百倍(耗费工夫嘛……我猜起码得多数千倍吧)。

不过,python 比起其它解释性语言还是强很多的。因为它能够当时把文本代码编译成“字节码”(存储于扩大名为 pyc 的文件里),从而间接解决整型的“指令代码”,不再须要从头开始剖析文本。

然而,从“字节码”翻译到理论 CPU 代码这步,依然是省不下的。

这个耗费,可看作“利用虚拟机”执行异构 CPU 上的程序。有人证实过,哪怕优化到极致,这也须要 10 倍的性能耗费。

这个耗费也有方法缩减。这就是 JIT 技术。

JIT 说白了,就是在第一遍执行一段代码前,先执行编译动作,而后执行编译后的代码。

如果代码中没有循环,那么这将白白付出很多额定的工夫代价;但若有肯定规模以上的循环,就可能节俭一点工夫。

这外面的佼佼者是 Java。它甚至能依据上次运行后果实时 profile,而后花大力量优化要害代码,从而失去比 C 更快的执行速度。

不过,现实很饱满,事实很骨感。尽管部分热点确实可能更快,但 Java 的整体效率依然比 C /C++ 差上很多——这个起因就比较复杂了。

和 C /C++/Java 那种投入海量资源通过千锤百炼的编译器不同,python 的 JIT 甚至可称得上“糟糕”。

加加减减,仅一个循环,慢上十几甚至几十倍还是很失常的。

以上探讨,仅仅思考了 for 循环这个控制结构自身。事实上,“慢”往往是全方位的。

举例来说,要计算一组向量,首先就要存储它。

怎么存储呢?

对 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 不敌对——原本能够间断存储的,当初……变成了个构造体;而且一旦存了某些类型的数据,就不得不通过指针跳转到另一块区域能力拜访(如果原地存储,节约的空间就太恐怖了)。

所以你看,咱要基于这种构造谈效率,是不是有点……

哪怕仅仅理解到这个水平也曾经很是惊心动魄了:解释执行 + 字节码优化慢上至多 10 倍到几十上百倍,“初学者敌对”的根底数据又慢上几倍到几十倍,透过容器拜访(而非性能更好的、固定大小数组乃至不查看下标伪装本人是数组的“内存区域”)再慢上几倍到几十倍……哪怕咱临时不思考其它机制带来的开销,仅把这几样往一块一凑(在某些特定的状况下,这些不同的“慢”点还可能相互影响、起到“缓慢度倍增放大”的成果)……

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

这些货色的相互影响极为简单奥妙,简直没人能彻底搞明确它。

你看,明确了前因后果,咱是不是只能说“python 的优化切实不错,才仅仅慢了 20 万倍而已”呢?(笑~

当然,如果不做这类较为简单的解决,仅仅是一些流程性的货色的话,这类语言的处理速度还是够用的——至多与之交互的人感触不到丝毫提早。

甚至,哪怕须要简单的解决,这类语言也能够向其它语言求救啊。就如同有个 numpy,谁敢说 python 做不了向量运算呢?

——当然,和里手谈话时,你得明确,这是找 C 之类语言搬救兵了。睁眼说瞎话把它当成 python 语言本人的能力是有点丢人的。不过如果只混 python 的圈子的话,这倒也不耽搁什么。

————————————————————————————

如果要揭短,业余程序员还会把无数据类型导致接口含糊所以无奈写较为简单的程序之类弊病给你列出一火车的。但这些就是没必要的题外话了。

毕竟,python 只是个胶水语言,初学者敌对并且应酬常见的简略利用场景入不敷出,这曾经足够了。

就如同把 office 做的傻瓜化,本就是业余程序员的工作一样——用户感觉好用、乐意掏钱就行了,何必关怀“做出一套 office 须要砸进去的钱足够盖 N 座迪拜塔”呢。

当然,如果想进一步倒退的话,请记住“在适合的中央用适合的工具”这句话——而后想方法搞明确每种工具的局限性吧。

毕竟,哪怕是 C /C++,在做矩阵之类运算时,也还会求助于 SIMD 的 MMX 指令、超线程 / 多外围 CPU 乃至 GPU,以便为本人“增补”上并行处理能力呢。

正文完
 0