百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

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

liebian365 2024-10-20 09:59 22 浏览 0 评论

我们先来看看,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,以便为自己“增补”上并行处理能力呢。

相关推荐

看黑客是如何获取你电脑最高权限的,一定要看

在渗透过程中,通过各种方式获取到一枚cmdshell,但是这个shell的权限比较低,无法让我们做我们想要做的一些操作,比如说获取系统密码,获取数据库信息,又或者比如说拿到服务器中的另一个站点的权限,...

是50个常用的Visual Basic代码示例:

以下是50个常用的VisualBasic代码示例:1.声明变量```vb...

电脑系统型号怎么看版本(如何看电脑系统型号)

有时候我们会需要进行查看电脑上安装的windows系统版本及系统版本号,但对于不懂电脑知识的小白来说要怎么查看电脑系统版本信息呢?别着急,有小编在接下来,就将查看电脑系统版本的教程来分享给你们,希望对...

dos命令systeminfo,查看系统启动时间。电脑卡慢,win10怎么了?

最近一段时间,有几个反应电脑卡慢的,都是windows10的系统。询问得知每天电脑有关机,打开任务管理器,内存使用量达到百分之九十多,而程序只打开微信、wps、360浏览器。cmd窗口运行命令syst...

systeminfo命令:全面解析系统信息!

你是否曾想过,仅凭一条简单的命令,就能深入了解计算机的"内心世界"?是不是有点不可思议?那么,让我们一起探寻这个神奇的命令,揭开它背后的奥秘吧!它能提供的信息超乎你的想象,从操作系统到硬件配置,再到驱...

电脑序列号怎么查询?只需两行命令一键查询

当我们的电脑出问题需要保修的时候,需要查询到电脑的型号和序列号才更便于进行下一步的操作,有包装盒的朋友还可以在包装盒上查询,笔记本用户可以在电脑底部标签上查询,没有包装盒和标签破损的用户就无从下手了。...

快速显示系统信息:Systeminfo命令详解

Systeminfo命令是windows系统中显示系统信息的命令,此命令可以显示出计算机的操作系统的详细配置信息,包括操作系统配置、安全信息、产品ID和硬件属性(如RAM、磁盘空间和网卡)。使用...

dos命令systeminfo图文教程,显示操作系统配置信息msinfo32

大家好,我是老盖,首先感谢观看本文,本篇文章做的有视频,视频讲述的比较详细,也可以看我发布的视频。今天我们学习systeminfo命令,该工具显示本地或远程机器(包括服务包级别)的操作系统配置的信息,...

基于uniapp+vue3跨端仿制chatgpt实例uniapp-chatgpt

#夏日生活打卡季#...

原创新作uniapp+vue3+pinia2高仿微信App聊天

前段时间有给大家分享一个flutter3.x桌面端os系统。今天再分享一款最新原创之作uniapp-vue3-wechat聊天实例。uni-vue3-wechat采用...

UniApp开发的设备适配(uniapp服务器配置)

UniApp是一个跨平台开发框架,支持多端应用(如H5、小程序、iOS、Android等)。由于不同设备的屏幕尺寸、分辨率、操作系统等存在差异,设备适配是开发过程中需要重点关注的问题。以下是Uni...

如何用服务器搭建自己的个人网站(自己服务器怎么做网站)

这篇教程主要是告诉大家如何利用TCP和HTTP协议来完成网站的搭建。首先你需要有C/C++语言基础,且有服务器、客户端概念,如果你了解TCP或者HTTP协议的话,那么将会帮助你更快的学会如何搭建个人网...

大话C语言:字符数组(c语言字符数组教学视频)

1字符数组概述C语言中没有字符串这种数据类型,可以通过char的数组来替代。数字0(和字符'\0'等价)结尾的char数组就是一个字符串,字符串是一种特殊的char的数组。...

源码分享:在pdf上加盖电子签章(pdf怎么加电子签章)

在pdf上加盖电子签章,并不是只是加个印章图片,。而是要使用一对密钥中的私钥对文件进行签字。为啥要用私钥呢?很简单,因为公钥是公开的,其他人才可以用公钥为你证明,这个文件是你签的。这就是我们常说的:私...

C语言wcstombs函数详解:宽字符字符串到多字节的「翻译官」

核心定位wcstombs是C语言中用于将宽字符字符串转换为多字节字符串的「翻译官」,它能将宽字符(wchar_t)转换为多字节字符(如UTF-8编码的中文)。就像一位翻译官,它能将一种语言(宽字符...

取消回复欢迎 发表评论: