代码之家  ›  专栏  ›  技术社区  ›  Adam Luchjenbroers

python性能特征

  •  14
  • Adam Luchjenbroers  · 技术社区  · 15 年前

    我正在调整我的一个宠物项目以提高它的性能。我已经完成了剖析器的工作,以确定热点,但我正在考虑更好地理解蟒蛇的性能特征,这在以后会非常有用。

    我想知道一些事情:

    它的优化器有多聪明?

    一些现代的编译器有着非常聪明的乐观主义者,他们可以经常使用简单的代码,使其比任何人类调试代码的尝试都运行得更快。取决于优化器有多聪明,我的代码最好是“哑”的。

    虽然python是一种“解释”语言,但它似乎编译成某种形式的字节码(.pyc)。这样做有多聪明?

    • 它会折叠常量吗?
    • 它是内联小函数还是展开短循环?
    • 它会执行复杂的数据/流分析吗?我没有资格正确解释。

    以下操作的速度(相对而言)

    • 函数调用
    • 类实例化
    • 算术
    • “较重”的数学运算,如sqrt()。

    内部如何处理数字?

    数字是如何存储在Python中的。它们是以整数/浮点数的形式存储在内部还是以字符串的形式移动?

    麻木的

    numpy能带来多少性能差异?这个应用程序大量使用向量和相关数学。使用它来加速这些操作可以产生多大的差异。

    还有什么有趣的吗

    如果你能想到任何值得知道的事情,请随意提出来。

    一些背景…

    因为有一些人提出了“先看你的算法”的建议(这是非常明智的建议,但对我提出这个问题的目的没有真正的帮助),我将在这里补充一些关于发生了什么以及为什么我要问这个问题。

    所讨论的pet项目是用python编写的一个光线跟踪程序。它还不远,目前只对场景中的两个对象(一个三角形和一个球体)进行了测试。未执行着色、阴影或照明计算。算法基本上是:

    for each x, y position in the image:
         create a ray
         hit test vs. sphere
         hit test vs. triangle
         colour the pixel based on the closest object, or black if no hit.
    

    光线跟踪中的算法改进通常通过尽早消除场景中的对象来工作。它们可以为复杂的场景提供相当大的推动力,但是如果光线跟踪器不费吹灰之力就无法在两个物体上进行测试,那么它就根本无法处理太多问题。

    虽然我意识到基于python的光线跟踪器不太可能达到基于c的光线跟踪器的性能,但考虑到实时光线跟踪器 Arauna 我可以在我的计算机上管理15-20 fps,以640x480的速度渲染相当复杂的场景,我希望用python渲染一个非常基本的500x500图像,可以在一秒钟内完成。

    目前,我的代码需要38秒。在我看来,这真的不应该花那么长时间。

    分析显示了在这些形状的实际命中测试例程中花费的大部分时间。这在光线跟踪器中并不特别令人惊讶,我所期望的。这些命中测试的调用计数为每个250000(准确地说是500x500),这表明它们被调用的频率与它们应该被调用的频率相同。这是一个相当不错的3%的文本书案例,其中优化是可取的。

    我正在计划在改进代码的过程中进行完整的计时/测量工作。然而,如果不知道在Python中什么代价,我试图调优代码的尝试只会在黑暗中跌跌撞撞。我想获得一点照亮道路的知识会对我很有帮助。

    6 回复  |  直到 15 年前
        1
  •  23
  •   Alex Martelli    15 年前

    python的编译器故意地非常简单——这使得它快速且高度可预测。除了一些持续的折叠之外,它基本上生成了忠实地模仿源代码的字节码。已经有人建议了 dis 这确实是一个很好的方法来查看您得到的字节码——例如,如何 for i in [1, 2, 3]: 实际上不是不断地进行折叠,而是动态地生成文本列表,而 for i in (1, 2, 3): (循环文本元组而不是文本列表) 能够持续折叠(原因:列表是一个可变的对象,为了保持“非常简单”的任务语句,编译器不需要检查这个特定的列表是否从未被修改过,所以 能够 优化成一个元组)。

    所以这里有足够的空间进行手动微优化——尤其是提升。即重写

    for x in whatever():
        anobj.amethod(x)
    

    作为

    f = anobj.amethod
    for x in whatever():
        f(x)
    

    保存重复的查找(编译器不检查 anobj.amethod 实际上可以改变 anobj 's bindings&c,以便下次需要新的查找--它只是做一些非常简单的事情,即不提升,这可以保证正确性,但绝对不能保证燃烧的速度;-)。

    这个 timeit 模块(最好在shell提示imho中使用)使得测量编译和字节码解释的整体效果变得非常简单(只要确保要测量的代码片段没有影响计时的副作用,因为 timeit 在循环中反复运行;-)。例如:

    $ python -mtimeit 'for x in (1, 2, 3): pass'
    1000000 loops, best of 3: 0.219 usec per loop
    $ python -mtimeit 'for x in [1, 2, 3]: pass'
    1000000 loops, best of 3: 0.512 usec per loop
    

    您可以看到重复列表构建的成本——并通过尝试一个小的调整来确认这确实是我们观察到的:

    $ python -mtimeit -s'Xs=[1,2,3]' 'for x in Xs: pass'
    1000000 loops, best of 3: 0.236 usec per loop
    $ python -mtimeit -s'Xs=(1,2,3)' 'for x in Xs: pass'
    1000000 loops, best of 3: 0.213 usec per loop
    

    将Iterable的结构移动到 -s 设置(只运行一次,不定时)表明,元组上的循环速度稍快(可能10%),但第一对的大问题(列表速度比元组慢100%以上)主要与构造有关。

    装备 时计 并且知道编译器在优化时故意非常简单,我们可以很容易地回答您的其他问题:

    下列操作有多快 (相对)

    * Function calls
    * Class instantiation
    * Arithmetic
    * 'Heavier' math operations such as sqrt()
    
    $ python -mtimeit -s'def f(): pass' 'f()'
    10000000 loops, best of 3: 0.192 usec per loop
    $ python -mtimeit -s'class o: pass' 'o()'
    1000000 loops, best of 3: 0.315 usec per loop
    $ python -mtimeit -s'class n(object): pass' 'n()'
    10000000 loops, best of 3: 0.18 usec per loop
    

    所以我们看到:实例化一个新样式的类和调用一个函数(都是空的)的速度大致相同,而实例化可能有一个很小的速度差,可能是5%;实例化一个旧样式的类的速度最慢(大约是50%)。微小的差别,如5%或更少当然可能是噪音,所以每次重复几次是明智的;但像50%的差别肯定远远超过噪音。

    $ python -mtimeit -s'from math import sqrt' 'sqrt(1.2)'
    1000000 loops, best of 3: 0.22 usec per loop
    $ python -mtimeit '1.2**0.5'
    10000000 loops, best of 3: 0.0363 usec per loop
    $ python -mtimeit '1.2*0.5'
    10000000 loops, best of 3: 0.0407 usec per loop
    

    在这里我们看到:打电话 sqrt 比由运算符执行相同的计算慢(使用 ** 通过调用一个空函数的成本来提高操作人员的能力;所有的算术操作人员的速度都大致相同,在噪声范围内(3或4纳秒的微小差别绝对是噪声;-)。检查持续折叠是否会干扰:

    $ python -mtimeit '1.2*0.5'
    10000000 loops, best of 3: 0.0407 usec per loop
    $ python -mtimeit -s'a=1.2; b=0.5' 'a*b'
    10000000 loops, best of 3: 0.0965 usec per loop
    $ python -mtimeit -s'a=1.2; b=0.5' 'a*0.5'
    10000000 loops, best of 3: 0.0957 usec per loop
    $ python -mtimeit -s'a=1.2; b=0.5' '1.2*b'
    10000000 loops, best of 3: 0.0932 usec per loop
    

    …我们看到事实确实如此:如果将两个数字中的一个或两个都作为变量进行查找(这会阻止不断折叠),我们将支付“现实”成本。变量查找有其自身的成本:

    $ python -mtimeit -s'a=1.2; b=0.5' 'a'
    10000000 loops, best of 3: 0.039 usec per loop
    

    不管怎样,当我们试图测量这么小的时间时,这绝不是微不足道的。的确 常数 查找也不是免费的:

    $ python -mtimeit -s'a=1.2; b=0.5' '1.2'
    10000000 loops, best of 3: 0.0225 usec per loop
    

    如您所见,虽然比变量查找小,但它是相当可比的——大约有一半。

    如果(有了仔细的分析和测量)您决定计算的一些核心非常需要优化,我建议您尝试 cython --这是一个C/python合并,它尝试着像python一样整洁,像c一样快,虽然它不能100%达到目标,但它确实是一个很好的拳头(特别是,它使二进制代码比前代语言快得多, pyrex 以及比它更富有一点)。对于性能的最后几个百分点,您可能仍然希望使用C(或者在某些特殊情况下使用汇编/机器代码),但这将是非常罕见的。

        2
  •  6
  •   Ned Batchelder    15 年前

    洛特是对的:主要影响是数据结构和算法。另外,如果你做了大量的I/O,你如何管理它将产生很大的影响。

    但是,如果您对编译器的内部结构感兴趣:它将折叠常量,但不会内联函数或展开循环。内嵌函数是动态语言中的一个难题。

    您可以通过分解一些已编译的代码来查看编译器的工作。在my_file.py中放入一些示例代码,然后使用:

    python -m dis my_file.py
    

    这个来源:

    def foo():
        return "BAR!"
    
    for i in [1,2,3]:
        print i, foo()
    

    生产:

      1           0 LOAD_CONST               0 (<code object foo at 01A0B380, file "\foo\bar.py", line 1>)
                  3 MAKE_FUNCTION            0
                  6 STORE_NAME               0 (foo)
    
      4           9 SETUP_LOOP              35 (to 47)
                 12 LOAD_CONST               1 (1)
                 15 LOAD_CONST               2 (2)
                 18 LOAD_CONST               3 (3)
                 21 BUILD_LIST               3
                 24 GET_ITER
            >>   25 FOR_ITER                18 (to 46)
                 28 STORE_NAME               1 (i)
    
      5          31 LOAD_NAME                1 (i)
                 34 PRINT_ITEM
                 35 LOAD_NAME                0 (foo)
                 38 CALL_FUNCTION            0
                 41 PRINT_ITEM
                 42 PRINT_NEWLINE
                 43 JUMP_ABSOLUTE           25
            >>   46 POP_BLOCK
            >>   47 LOAD_CONST               4 (None)
                 50 RETURN_VALUE
    

    请注意,只有模块中的顶级代码是反汇编的,如果希望看到反汇编的函数定义,您还需要自己编写一些代码,以便在嵌套的代码对象中循环。

        3
  •  6
  •   Eric O. Lebigot    15 年前

    通过使用 Psyco 模块。

    至于numpy,它通常会加速一个重要因素。我认为在操作数字数组时必须这样做。

    您可能还希望使用 Cython Pyrex 它允许您创建更快的扩展模块,而不必用C语言编写一个完整的扩展模块(这会更麻烦)。

        4
  •  4
  •   S.Lott    15 年前

    这是有趣的。

    • 数据结构

    • 算法

    这些将产生巨大的改善。

    您的列表有助于(最多)提高几个位数的性能。

    如果您想看到真正的速度改进,就需要从根本上重新考虑数据结构。

        5
  •  4
  •   Ned Batchelder    15 年前

    如果您已经知道您的算法尽可能快,并且您知道C会快得多,那么您可能希望在C中实现代码的核心作为 C extension to Python . 您可以实用主义地决定代码的哪一部分使用C语言,哪一部分使用Python语言,并充分利用每种语言的潜力。

    与其他语言不同,C和Python之间的调用速度非常快,因此经常跨越边界不会受到惩罚。

        6
  •  4
  •   Jacco Bikker    15 年前

    我是《阿劳娜》的作者。我对Python一无所知,但我知道Arauna非常优化,包括高级(数据结构和算法)和低级(缓存友好代码、SIMD和多线程)。这是一个很难达到的目标…