我正在调整我的宠物项目以改善其性能。我已经淘汰了剖析器以识别热点,但我认为理解Pythons的性能特性会更好一点,这对今后非常有用。
我想知道一些事情:
一些现代编译器拥有非常聪明的优化器,通常可以使用简单的代码并使其运行速度比任何人类调整代码的尝试都要快。根据优化器的智能程度,我的代码可能要好得多“哑”。
虽然Python是一种“解释”语言,但它确实可以编译成某种形式的字节码(.pyc)。这样做有多聪明?
如何在Python中存储数字。它们是在内部存储为整数/浮点数还是以字符串形式移动?
NumPy可以带来多少性能差异?该应用程序大量使用向量和相关数学。使用它来加速这些操作可以产生多大的差异。
如果你能想到任何值得了解的事情,请随时提及。
由于有一些人引入了“先查看你的算法”的建议(这是非常明智的建议,但对我提出这个问题的目的并没有帮助)我会在这里添加一些关于什么的继续,为什么我问这个。
有问题的宠物项目是用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秒。在我看来,它真的不应该花那么长时间。
分析显示了在这些形状的实际命中测试例程中花费的大部分时间。这在光线追踪器中并不是特别令人惊讶,也是我所期待的。这些命中测试的呼叫计数分别为250,000(精确到500x500),这表明它们的调用次数与它们应该的频率相同。这是一个很好的教科书案例3%,建议进行优化。
当我致力于改进代码时,我正计划完成全部计时/测量工作。然而,如果没有一些关于Python成本的基本知识,我试图调整我的代码只会在黑暗中磕磕绊绊。我认为通过照亮方式获得一些知识对我很有帮助。
答案 0 :(得分:23)
Python的编译器故意简单 - 这使得它快速且高度可预测。除了一些常量折叠,它基本上生成字节码,忠实地模仿你的来源。其他人已经建议dis,这确实是查看你得到的字节码的一种好方法 - 例如,for i in [1, 2, 3]:
实际上并没有进行常量折叠,而是生成文字列表。 fly,while for i in (1, 2, 3):
(循环使用文字元组而不是文字列表) 能够进行常数折叠(原因:列表是一个可变对象,并保持“污垢” -simple“任务语句,编译器不会费心去检查这个特定列表是否永远不会被修改,因此可以优化为元组。”
因此,有足够的空间进行充分的手动微量优化 - 尤其是提升。即,重写
for x in whatever():
anobj.amethod(x)
作为
f = anobj.amethod
for x in whatever():
f(x)
保存重复的查找(编译器不检查anobj.amethod
的运行是否可以实际更改anobj
的绑定& 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%以上)主要是建设。
有timeit
武装并且知道编译器在优化过程中故意非常简单,我们可以轻松回答你的其他问题:
以下操作的速度有多快 (比较)
* 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
比运算符(使用**
up-to-power运算符)执行相同的计算慢得多,大致是调用空函数的成本;所有算术运算符的速度与噪声内的速度大致相同(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(或者在某些特殊情况下的汇编/机器代码),但那真的非常罕见。
答案 1 :(得分:6)
S.Lott是对的:最重要的影响是数据结构和算法。此外,如果您正在进行大量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
请注意,只有模块中的顶级代码被反汇编,如果你想看到反汇编的函数定义,你需要自己编写更多代码来通过嵌套代码对象进行递归。
答案 2 :(得分:6)
使用Psyco模块可以自动提高代码的速度。
至于Numpy,它通常会加速一个重要因素。在操作数值数组时,我认为这是必须的。
您可能还希望使用Cython或Pyrex来加速代码的关键部分,这样您就可以创建更快的扩展模块,而无需在C中编写完整的扩展模块(哪个会比较麻烦。)
答案 3 :(得分:4)
这是有趣的。
数据结构
算法
这些将产生显着的改善。
您的列表最好是 - 只需几位数的性能提升。
如果您希望看到真正的速度提升,则需要从根本上重新考虑您的数据结构。
答案 4 :(得分:4)
如果您已经知道您的算法尽可能快,而且您知道C会快得多,那么您可能希望在C中将代码的核心实现为C extension to Python。您可以实际决定使用每种语言充分发挥C语言的哪一部分在C中,哪些部分在Python中。
与其他一些语言不同,C和Python之间的调用速度非常快,因此经常跨越边界不会受到惩罚。
答案 5 :(得分:4)
我是Arauna的作者。我对Python一无所知,但我确实知道Arauna是非常优化的,包括高级(数据结构和算法)和低级(缓存友好代码,SIMD,多线程)。这是一个艰难的目标......