眼下 Python 反常火爆,不论是 DevOps、数据科学、Web 开发仍是安全范畴,都在用 Python——可是它在速度上却没有任何优势。
与 C、C++、C# 或 Python 比较,Java 的速度怎样?答案很大程度上依赖于你需求运转的运用品种。世上没有完美的功能测验,但核算机言语评测游戏(Computer Language Benchmarks Game)是个很好的测验办法:http://algs4.cs.princeton.edu/faq/。
我从十年前就开端议论核算机言语评测游戏。与 Java、C#、Go、JavaScript、C++ 等其他言语比较,Python 是最慢的言语之一。这儿包括JIT(Just In Time)言语(如C#、Java)和 AOT(Ahead Of Time)言语(C、C++)编译器,也有 JavaScript 这种解说言语。
注:本文中所说的“Python”是指言语的具体完结,即 CPython。本文也会说到其他运转。
我期望答复以下问题:假如 Python 完结相同的使命要花费其他言语二至十倍的时刻,那么它为什么慢,能不能更快一些呢?
以下是几种常见的原因:
“由于它是GIL(大局解说器锁)”
“由于它是解说言语不是编译言语”
“由于它是动态类型言语”
终究哪个原因对功能的影响最大?
“由于它是GIL”
现代核算机的 CPU 有多个中心,有时甚至有多个处理器。为了运用全部核算才能,操作体系界说了一个底层结构,叫做线程,而一个进程(例如 Chrome浏览器)能够生成多个线程,经过线程来履行体系指令。这样假如一个进程是要运用许多 CPU,那么核算负载就会由多个中心分管,终究使得绝大大都运用能更快地完结使命。
在编撰本文时,我的 Chrome 浏览器开了 44 个线程。别的,根据 POSIX 的操作体系(如 Mac OS 和 Linux)的线程结构和 API 与 Windows 操作体系是不相同的。操作体系还担任线程的调度。
假如你没写过多线程程序,那么你应该了解一下锁的概念。与单线程进程不同,在多线程编程中,你要保证改动内存中的变量时,多个线程不会企图一起修正或拜访同一个内存地址。
CPython 在创立变量时会分配内存,然后用一个计数器核算对该变量的引证的次数。这个概念叫做“引证计数”。假如引证的数目为 0,那就能够将这个变量从体系中释放掉。这样,创立“暂时”变量(如在 for 循环的上下文环境中)不会耗光运用程序的内存。
随之而来的问题就是,假如变量在多个线程中同享,CPython 需求对引证计数器加锁。有一个“大局解说器锁”会慎重地操控线程的履行。不论有多少个线程,解说器一次只能履行一个操作。
这对 Python 运用的功能有什么影响?
假如运用程序是单线程、单解说器的,那么这不会对速度有任何影响。去掉 GIL 也不会影响代码的功能。
但假如想用一个解说器(一个 Python 进程)经过线程完结并发,而且线程是IO 密集型的(即有许多网络输入输出或磁盘输入输出),那么就会呈现下面这种 GIL 竞赛:
来自于David Beazley的“图解GIL”一文:http://dabeaz.blogspot.com/2010/01/python-gil-visualized.html
假如 Web 运用(如 Django)运用了 WSGI,那么发往 Web 运用的每个恳求都会由独立的 Python 解说器履行,因而每个恳求都只会有一个锁。由于 Python 解说器发动很慢,一些 WSGI 完结就支撑“看护形式”,坚持 Python 进程长时刻运转。
其他 Python 运转时怎样?
PyPy 的 GIL 一般要比 CPython 快三倍以上。
Jython 没有 GIL 由于 Jython 中的 Python 线程由 Java 线程表明,因而能享用到 JVM 内存办理体系的优点。
JavaScript 怎样处理这个问题i?
首要,全部 JavaScript 引擎都是用符号-铲除废物收回算法。如前所述,对 GIL 的需求主要是由 CPython 的内存办理算法导致的。
JavaScript 没有 GIL,但它也是单线程的,所以它底子不需求。JavaScript 的时刻循环和 Promise/Callback 形式完结了异步编程,代替了并发编程。Python 也能经过 asyncio 的作业循环完结相似的形式。
“由于它是解说言语”
这条理由我也听过许多,我发现它过于简化了 CPython 的实际作业原理。当你在终端上写 python myscript.py 时,CPython 会发动一长串操作,包括读取、词法剖析、语法剖析、编译、解说以及履行。
假如你对这些进程感兴趣,能够看看我之前写的文章:
6分钟修正Python言语:https://hackernoon.com/modifying-the-python-language-in-7-minutes-b94b0a99ce14
这个进程的要点就是它会在编译阶段生成.pyc文件,字节码会写到__pycache__/下的文件中(假如是Python 3),或许写到与源代码同一个目录中(Python 2)。不只你编写的脚本是这样,全部你导入的代码都是这样,包括第三方模块。
因而绝大大都情况下(除非你写的代码只会运转一次),Python是在解说字节码并在本地履行。与Java和C#.NET比较一下:
Java将源代码编译成“中心言语”,然后Java虚拟机读取字节码并即时编译成机器码。.NET CIL也是相同的,.NET的公共言语运转时(CLR)运用即时编译将字节码编译成机器码。
那么,已然它们都运用虚拟机,以及某种字节码,为什么Python在功能测验中比Java和C#慢那么多?第一个原因是,.NET和Java是即时编译的(JIT)。
即时编译,即JIT(Just-in-time),需求一种中心言语,将代码分割成小块(或许称帧)。而提早编译(Ahead of Time,简称AOT)是编译器把源代码翻译成CPU能了解的代码之后再履行。
JIT自身并不能让履行更快,由于它履行的是相同的字节码序列。可是,JIT能够在运转时做出优化。好的GIT优化器能找到运用程序中履行最多的部分,称为“热门”。然后对那些字节码进行优化,将它们替换成功率更高的代码。
这就是说,假如你的运用程序会重复做某件作业,那么速度就会快许多。此外,别忘了Java和C#都是强类型言语,所以优化器能够对代码做更多的假定。
前面说过,PyPy有个JIT,因而它比CPython要快许多。下面这篇功能测验的文章介绍得更具体:
哪个版别的Python最快?https://hackernoon.com/which-is-the-fastest-version-of-python-2ae7c61a6b2b
那么为什么CPython不必JIT?
JIT也有缺陷:首要就是发动速度。CPython的发动速度现已比较慢了,而PyPy的发动速度要比CPython慢两到三倍。Java虚拟机的发动速度也是出了名的慢。.NET CLR在体系发动时发动,因而避免了这个问题,但这要归功于CLR和操作体系是同一拨开发者开发的。
假如你有一个Python进程需求运转很长时刻,而且代码里包括“热门”能够被优化,那么运用JIT就很不错。
可是,CPython是个通用的完结。因而假如要用Python开发命令行程序,那么每次都要等候JIT调用CLI就特别慢了。
CPython企图满意大部分情况下的需求。有一个在CPython中完结JIT(https://www.slideshare.net/AnthonyShaw5/pyjion-a-jit-extension-system-for-cpython)的项目,不过这个项目现已中止很久了。
假如你想要享用JIT的优点,而且要处理的使命合适JIT,那就运用PyPy。
“由于它是动态类型言语”
“静态类型”言语要求必须在变量界说时指定其类型,例如C、C++、Java、C#和Go等。
而动态类型言语中虽然也有类型的概念,但变量的类型是动态的。
在这个比如中,Python用相同的姓名和str类型界说了第二个变量,一起释放了第一个a的实例占用的内存。
静态类型言语的规划意图并不是折磨人,这样规划是由于CPU就是这样作业的。假如任何操作终究都要转化成简略的二进制操作,那就需求将目标和类型都变换成初级数据结构。
Python帮你做了这全部,只不过你从来没有关怀过,也不需求关怀。
不需求界说类型并不是Python慢的原因。Python的规划能够让你把全部都做成动态的。你能够在运转时替换目标的办法,能够在运转时给底层体系调用打补丁。简直全部都有可能。
而这种规划使得Python的优化变得很困难。
为了演示这个观念,我运用了一个Mac OS下的体系调用盯梢东西,叫做Dtrace。CPython的发布并不支撑DTrace,因而需求从头编译CPython。演示顶用的是Python 3.6.6:
现在Python.exe的代码中包括了Dtrace的盯梢代码。Paul Ross有一篇非常好的关于DTrace的讲演(https://github.com/paulross/dtrace-py#the-lightning-talk)。能够从这儿下载DTrace用于Python的文件(https://github.com/paulross/dtrace-py/tree/master/toolkit)用来丈量函数调用、履行时刻、CPU时刻、体系调用以及各种函数等等。
py_callflow盯梢器会显现运用程序的全部函数调用。
那么,Python的动态类型是否让Python更慢?
比较并变换类型的价值很大。每次读取、写入或引证变脸时都会查看类型
动态类型的言语很难优化。许多代替Python的言语很快的原因就是它们献身了便利性来交流功能。
例如Cython(http://cython.org/),它经过结合C的静态类型和Python的办法,使得代码中的类型已知,然后优化代码,能够取得84倍的功能提高(http://notes-on-cython.readthedocs.io/en/latest/std_dev.html)
定论
Python慢的主要原因是由于它的动态和多样性。它能用于处理各种问题,但大都问题都有优化得更好和更快的处理方案。
但Python运用也有许多优化办法,如运用异步、了解功能测验东西,以及运用多解说器等。
关于发动时刻不重要,而代码可能享用到JIT的优点的运用,能够考虑运用PyPy。
关于代码中功能很重要的部分,假如变量大多是静态类型,能够考虑运用Cython。