是否存在技术限制或语言功能阻止我的Python脚本像等效的C ++程序一样快?


10

我是Python的长期用户。几年前,我开始学习C ++,以了解它在速度方面可以提供什么。在这段时间里,我将继续使用Python作为原型制作工具。看来,这是一个很好的系统:使用Python进行敏捷开发,在C ++中快速执行。

最近,我越来越多地使用Python,并学习如何避免早年使用该语言时很快使用的所有陷阱和反模式。据我了解,使用某些功能(列表理解,枚举等)可以提高性能。

但是,是否存在技术限制或语言功能使我的Python脚本无法与同等C ++程序一样快?


2
是的,它可以。有关Python编译器的最新技术,请参见PyPy
格雷格·休吉尔

5
python中的所有变量都是多态的,这意味着变量的类型仅在运行时才知道。如果您在类似C的语言中看到(假设整数)x + y,它们会进行整数加法运算。在python中,将对x和y上的变量类型进行切换,然后选择适当的加法函数,然后将进行溢出检查,然后进行加法。除非python学习静态类型,否则这种开销永远不会消失。
nwp

1
@nwp不,很简单,请参阅PyPy。棘手但仍未解决的问题包括:如何克服JIT编译器的启动延迟,如何避免为复杂的长期对象图分配内存以及通常如何充分利用缓存。

Answers:


11

几年前我从事全职Python编程工作时,自己有点碰壁。我确实喜欢Python,但是当我开始进行一些性能调整时,我受到了一些粗鲁的冲击。

严格的Pythonista使用者可以纠正我的问题,但是以下是我发现的内容,这些内容以非常广泛的笔触进行了描绘。

  • Python内存使用情况有点吓人。Python将所有内容都表示为dict-功能非常强大,但结果是,即使简单的数据类型也是巨大的。我记得字符“ a”占用了28个字节的内存。如果您在Python中使用大数据结构,请确保依赖numpy或scipy,因为它们由直接字节数组实现支持。

这会影响性能,因为与其他语言相比,这意味着在运行时还会有更多级别的间接访问,此外还会浪费大量内存。

  • Python确实具有全局解释器锁,这意味着在大多数情况下,进程正在运行单线程。可能有一些库在各个进程之间分配任务,但是我们正在整理32个左右的python脚本实例并运行每个线程。

其他人可以与执行模型对话,但是Python是在运行时进行编译然后进行解释的,这意味着它并不能一直用于机器代码。这也会对性能产生影响。您可以轻松地链接C或C ++模块,或者找到它们,但是如果您直接运行Python,它将对性能产生很大的影响。

现在,在Web服务基准测试中,Python比其他运行时编译语言(如Ruby或PHP)优越。但这远远落后于大多数编译语言。即使是编译成中间语言并在VM中运行的语言(如Java或C#),也要好得多。

这是我偶尔提到的一组非常有趣的基准测试:

http://www.techempower.com/benchmarks/

(尽管如此,我仍然非常喜欢Python,如果我有机会选择自己使用的语言,那将是我的第一选择。在大多数情况下,无论如何我都不受疯狂的吞吐量要求的约束。)


2
字符串“ a”不是第一个要点的好示例。Java字符串对于单个字符串也有相当大的开销,但是随着字符串长度的增加(取决于版本,构建选项和字符串内容,一个或四个字节的字符),它的固定开销会相当不错地摊销。不过,您对用户定义的对象是正确的,至少是那些不使用的对象__slots__。PyPy在这方面应该做得更好,但我还不足以判断。

1
您要指出的第二个问题仅与特定的实现有关,而与语言无关。第一个问题需要解释:“重” 28个字节不是字符本身,而是打包在字符串类中的事实,带有它自己的方法和属性。在Python 3.3上,将单个字符表示为字节数组(字面b'a')仅重18字节,而且我确信如果您的应用程序确实需要,还有更多方法可以优化内存中的字符存储。
红色

C#可以本地编译(例如即将推出的MS技术,适用于iOS的Xamarin)。

13

Python参考实现是“ CPython”解释器。它尝试相当快,但目前未采用高级优化。对于许多使用场景而言,这是一件好事:对某些中间代码的编译会在运行时之前立即进行,并且每次执行程序时都会重新编译一次代码。因此,必须将优化所需的时间与优化所获得的时间进行权衡–如果没有净收益,则优化毫无价值。对于运行时间非常长的程序或循环非常紧密的程序,采用高级优化将很有用。但是,CPython用于某些无法进行积极优化的工作:

  • 短运行脚本,例如用于sysadmin任务。像Ubuntu这样的许多操作系统都在Python的基础上构建了很大一部分基础架构:CPython足够快地完成这项工作,但是实际上没有启动时间。只要它比bash快,那就好。

  • CPython必须具有清晰的语义,因为它是参考实现。这允许进行简单的优化,例如“优化foo运算符的实现”或“将列表理解编译为更快的字节码”,但通常会排除破坏信息的优化,例如内联函数。

当然,除了CPython之外,还有更多的Python实现:

  • Jython构建在JVM之上。JVM可以解释或JIT编译提供的字节码,并具有配置文件引导的优化。它的启动时间很长,而且要花一点时间才能启动JIT。

  • PyPy是最先进的JITting Python VM。PyPy用RPython(Python的受限子集)编写。该子集从Python中删除了一些表达性,但允许静态推断任何变量的类型。然后,可以将用RPython编写的VM转换为C,从而获得与RPython类似的C性能。但是,RPython仍比C具有更高的表现力,从而可以更快地开发新的优化程序。PyPy是编译器自举的一个示例。PyPy(不是RPython!)主要与CPython参考实现兼容。

  • Cython(与RPython一样)是具有静态类型的不兼容Python方言。它还可以转换为C代码,并且能够轻松地为CPython解释器生成C扩展。

如果您愿意将Python代码转换为Cython或RPython,那么您将获得类似C的性能。但是,不应将它们理解为“ Python的子集”,而应理解为“具有Pythonic语法的C”。如果您切换到PyPy,您的原始Python代码将获得可观的速度提升,但也将无法与用C或C ++编写的扩展接口。

但是,除了启动时间长之外,还有哪些属性或功能会阻止香草Python达到类似C的性能水平?

  • 贡献者和资金。与Java或C#不同,在该语言的背后没有一家独立的驱动公司有兴趣使该语言成为同类产品中的佼佼者。这将发展限制在主要限于志愿者和偶尔的赠款上。

  • 后期绑定和缺少任何静态类型。Python允许我们这样编写废话:

    import random
    
    # foo is a function that returns an empty list
    def foo(): return []
    
    # foo is a function, right?
    # this ought to be equivalent to "bar = foo"
    def bar(): return foo()
    
    # ooh, we can reassign variables to a different type – randomly
    if random.randint(0, 1):
       foo = 42
    
    print bar()
    # why does this blow up (in 50% of cases)?
    # "foo" was a function while "bar" was defined!
    # ah, the joys of late binding

    在Python中,可以随时重新分配任何变量。这样可以防止缓存或内联;任何访问都必须通过变量。这种间接影响了性能。当然:如果您的代码没有做这些疯狂的事情,以便可以在编译之前为每个变量指定确定的类型,并且每个变量只分配一次,那么从理论上讲,可以选择一个更有效的执行模型。考虑到这一点的语言将提供一些将标识符标记为常量的方法,并且至少允许使用可选的类型注释(“渐进式键入”)。

  • 可疑的对象模型。除非使用插槽,否则很难确定对象具有哪些字段(Python对象本质上是字段的哈希表)。即使到了那里,我们仍然不知道这些字段的类型。这样可以防止将对象表示为紧密打包的结构,就像C ++中那样。(当然,C ++的对象表示也不理想:由于类似结构的性质,即使私有字段也属于对象的公共接口。)

  • 垃圾收集。在许多情况下,可以完全避免使用GC。C ++允许我们静态分配对象,这些对象在离开当前范围时会自动销毁:Type instance(args);。在此之前,该对象是活动的,可以借给其他功能。这通常是通过“引用传递”完成的。诸如Rust之类的语言允许编译器静态地检查指向该对象的指针是否没有超出该对象的寿命。这种内存管理方案是完全可预测的,高效的,并且适合大多数情况下没有复杂对象图的情况。不幸的是,Python在设计时并未考虑内存管理。从理论上讲,逸出分析可用于查找可以避免GC的情况。实际上,简单的方法链例如foo().bar().baz() 将必须在堆上分配大量的短期对象(世代GC是使此问题保持较小的一种方法)。

    在其他情况下,程序员可能已经知道某些对象(如列表)的最终大小。不幸的是,Python在创建新列表时没有提供传达此信息的方法。相反,新项目将被推送到最后,这可能需要多个重新分配。一些注意事项:

    • 可以创建特定大小的列表,例如fixed_size = [None] * size。但是,该列表中对象的内存将必须单独分配。对比C ++,我们可以做到std::array<Type, size> fixed_size

    • 可以通过array内置模块在Python中创建特定本机类型的压缩数组。此外,numpy还提供了具有本机数字类型特定形状的数据缓冲区的有效表示。

摘要

Python被设计为易于使用,而非性能。它的设计使创建高效的实现变得相当困难。如果程序员放弃了有问题的功能,那么理解其余习语的编译器将能够发出可与C媲美的高效代码。


8

是。主要的问题是该语言被定义为动态的-也就是说,直到您要执行该操作之前,您才知道自己在做什么。这使得它很难产生高效的机器代码,因为你不知道生产什么机器代码。JIT编译器可以在这方面做一些工作,但是它永远无法与C ++相提并论,因为JIT编译器根本无法花费时间和内存来运行,因为那是您无需花费时间来运行程序的时间和内存,并且对什么内容有严格的限制它们可以在不破坏动态语言语义的情况下实现。

我不会断言这是不可接受的折衷。但是,对于Python的本质而言,基本的实现永远不会比C ++的实现快。


8

影响所有动态语言性能的三个主要因素比其他因素更为重要。

  1. 解释性开销。在运行时,存在某种字节代码而不是机器指令,并且执行此代码有固定的开销。
  2. 调度开销。直到运行时才知道函数调用的目标,并且找出要调用的方法会带来成本。
  3. 内存管理开销。动态语言将内容存储在必须分配和释放的对象中,这会带来性能开销。

对于C / C ++,这三个因素的相对成本几乎为零。指令直接由处理器执行,调度最多需要一个或两个间接调用,除非您这样说,否则永远不会分配堆内存。编写良好的代码可能会采用汇编语言。

对于使用JIT编译的C#/ Java,前两个比较低,但是垃圾回收的内存成本很高。编写良好的代码可能接近2倍的C / C ++。

对于Python / Ruby / Perl,这三个因素的成本都相对较高。认为是C / C ++的5倍或更差。(*)

请记住,运行时库代码很可能用与您的程序相同的语言编写,并且具有相同的性能限制。


(*)随着Just-In_Time(JIT)编译扩展到这些语言,它们也将接近(通常为2倍)编写良好的C / C ++代码的速度。

还应注意的是,一旦差距缩小(在竞争的语言之间),差异就由算法和实现细节主导。JIT代码可能胜过C / C ++,而C / C ++可能胜过汇编语言,因为编写好代码更容易。


“请记住,运行时库代码很可能用与您的程序相同的语言编写,并且具有相同的性能限制。” 和“对于Python / Ruby / Perl,这三个因素的成本都相对较高。与C / C ++相比,认为成本是其5倍或更低。” 实际上,这是不正确的。例如,Rubinius Hash类(Ruby的核心数据结构之一)是用Ruby编写的,它的性能与Hash用C编写的YARV的类相当,有时甚至更快。原因之一是Rubinius的大部分运行时系统是用Ruby编写的,让他们可以...
约尔格W¯¯米塔格

…例如,Rubinius编译器内联。极端的例子是Klein VM(用于Self的元循环VM)和Maxine VM(用于Java的元循环VM),其中所有内容,甚至是方法分派代码,垃圾收集器,内存分配器,原始类型,核心数据结构和算法都写在其中。自我或Java。这样,即使核心VM的一部分也可以内联到用户代码中,并且VM可以使用来自用户程序的运行时反馈来重新编译和重新优化自身。
约尔格W¯¯米塔格

@JörgWMittag:仍然如此。Rubinius拥有JIT,并且在单个基准测试中,JIT代码通常会击败C / C ++。我找不到任何证据可以证明,在没有JIT的情况下,这种准圆形的东西对速度有很大的帮助。[有关JIT的清晰性,请参见编辑。]
david.pfx 2014年

1

但是,是否存在技术限制或语言功能使我的Python脚本无法与同等C ++程序一样快?

否。这只是投入大量资金和资源以使C ++快速运行与投入资金和资源以使Python快速运行有关的问题。

例如,当自助虚拟机问世时,它不仅是最快的动态OO语言,而且是最快的OO语言时期。尽管它是一种令人难以置信的动态语言(例如,比Python,Ruby,PHP或JavaScript还要多),但是它比大多数可用的C ++实现要快。

但是随后Sun取消了Self项目(一种用于开发大型系统的成熟的通用OO语言),而只专注于电视机顶盒中的动画菜单的小型脚本语言(您可能已经听说过,称为Java),当时没有更多资金。同时,英特尔,IBM,微软,Sun,Metrowerks,HP等。花了大量的金钱和资源来提高C ++的速度。CPU制造商在其芯片中添加了功能,以加快C ++的速度。编写或修改了操作系统,以使C ++更快。因此,C ++速度很快。

我不是非常熟悉Python,我是Ruby的人,所以我将举一个Ruby的例子:Rubinius Ruby实现中的Hash类(在功能和重要性上dict与Python 等效)是用100%纯Ruby编写的;但是它的竞争非常好,有时甚至胜过Hash用手工优化的C语言编写的YARV中的类。并且与某些商用的Lisp或Smalltalk系统(或前面提到的Self VM)相比,Rubinius的编译器甚至都不那么聪明。

Python没有内在的特性可以使它变慢。当今的处理器和操作系统中有一些功能会损害Python(例如,众所周知,虚拟内存对于垃圾收集性能非常糟糕)。有一些功能可以帮助C ++但不能帮助Python(现代CPU试图避免高速缓存未命中,因为它们是如此昂贵。不幸的是,当您拥有OO和多态性时,避免高速缓存未命中很难。相反,您应该降低高速缓存的成本专为Java设计的Azul Vega CPU做到了这一点。

如果您像在C ++上一样花大量的金钱,研究和资源来使Python快速运行,而在使像Python上那样快速运行Python的操作系统上花费了同样的钱,研究和资源,那么您就花了在使CPU能够像C ++一样快速运行的CPU方面投入了大量金钱,研究和资源,因此,毫无疑问,Python可以达到与C ++相当的性能。

我们已经用ECMAScript看到了如果只有一名玩家认真对待性能会发生什么。一年之内,我们所有主要供应商的整体性能基本上提高了10倍。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.