为什么Python对于简单的for循环来说太慢了?

时间:2011-11-11 17:03:57

标签: python performance jit

我们正在Python中进行一些kNNSVD实现。其他人选择了Java。我们的执行时间非常不同。我使用cProfile来查看我犯错误的地方但实际上一切都很fine。是的,我也使用numpy。但我想问一个简单的问题。

total = 0.0
for i in range(9999): # xrange is slower according 
    for j in range(1, 9999):            #to my test but more memory-friendly.
        total += (i / j)
print total

此代码段在我的计算机上占用31.40秒。

此代码的Java版本在同一台计算机上占用1秒或更短时间。我想,类型检查是这段代码的主要问题。但我应该为我的项目做很多这样的操作,我认为9999 * 9999不是那么大的数字。

我认为我犯了错误,因为我知道Python被许多科学项目所使用。但是为什么这段代码这么慢,我怎么能处理比这更大的问题?

我应该使用JIT编译器,例如Psyco吗?

修改

我也说这个循环问题只是一个例子。代码并不像这样简单,并且可能很难将改进/代码示例付诸实践。

另一个问题是,我可以实施大量数据挖掘吗?如果我使用正确的机器学习算法numpyscipy

11 个答案:

答案 0 :(得分:32)

  

我认为我犯了错误,因为我知道Python被很多科学项目所使用。

他们大量使用SciPy(NumPy是最突出的组件,但我听说围绕NumPy的API开发的生态系统更为重要),大大加速了各种操作这些项目需要。你做错了什么:你没有在C中编写你的关键代码。一般来说,Python非常适合开发,但是扩展良好的扩展模块本身就是一个至关重要的优化(至少当你正在处理数字时)。 Python是一种非常糟糕的语言,用于实现紧密的内部循环。

默认(当时最受欢迎和广泛支持的)实现是一个简单的字节码解释器。即使是最简单的操作,如整数除法,也可能需要数百个CPU周期,多个内存访问(类型检查是一个流行的例子),几个C函数调用等,而不是几个(甚至单个,在整数的情况下)分裂)指令。此外,该语言设计有许多抽象,增加了开销。如果你使用xrange,你的循环会在堆上分配9999个对象 - 如果使用range(9999 * 9999整数减去大约256 * 256,对于缓存的小整数),则更多。此外,xrange版本会在每次迭代时调用一个方法来推进 - 如果序列上的迭代未经过特定优化,那么range版本也是如此。它仍然需要一个完整的字节码调度,这本身就非常复杂(当然,与整数除法相比)。

看看JIT是多么有趣(我推荐PyPy而不是Psyco,后者不再开发,而且范围非常有限 - 尽管如此,它可能适用于这个简单的例子)。经过一小部分迭代之后,它应该产生一个最优的机器代码循环,增加一些防护 - 简单的整数比较,如果它们失败则跳跃 - 以保持正确性,以防你在该列表中得到一个字符串。 Java可以做到同样的事情,只是更早(它不必先跟踪)和更少的防护(至少如果你使用int s)。这就是它快得多的原因。

答案 1 :(得分:14)

因为您提到了科学代码,请查看numpy。您正在做的事情可能已经完成(或者更确切地说,它使用LAPACK来处理像SVD这样的事情)。当你听说python被用于科学代码时,人们可能并没有像你在你的例子中那样使用它。

作为一个简单的例子:

(如果您使用的是python3,您的示例将使用浮点除法。我的示例假设您使用的是python2.x,因此使用整数除法。如果不是,请指定i = np.arange(9999, dtype=np.float)等)

import numpy as np
i = np.arange(9999)
j = np.arange(1, 9999)
print np.divide.outer(i,j).sum()

给出时间的一些想法...(我将在这里使用浮点除法,而不是像你的例子中的整数除法):

import numpy as np

def f1(num):
    total = 0.0
    for i in range(num): 
        for j in range(1, num):
            total += (float(i) / j)
    return total

def f2(num):
    i = np.arange(num, dtype=np.float)
    j = np.arange(1, num, dtype=np.float)
    return np.divide.outer(i, j).sum()

def f3(num):
    """Less memory-hungry (and faster) version of f2."""
    total = 0.0
    j = np.arange(1, num, dtype=np.float)
    for i in xrange(num):
        total += (i / j).sum()
    return total

如果我们比较时间:

In [30]: %timeit f1(9999)
1 loops, best of 3: 27.2 s per loop

In [31]: %timeit f2(9999)
1 loops, best of 3: 1.46 s per loop

In [32]: %timeit f3(9999)
1 loops, best of 3: 915 ms per loop

答案 2 :(得分:6)

我认为NumPy的循环速度可能比CPython快(我没有在PyPy中进行测试)。

我想从Joe Kington的代码开始,因为此答案使用了NumPy。

%timeit f3(9999)
704 ms ± 2.33 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

我自己:

def f4(num):
    x=np.ones(num-1)
    y=np.arange(1,num)
    return np.sum(np.true_divide(x,y))*np.sum(y)

155 µs ± 284 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

此外,高中数学可以将问题简化为计算机。

Problem= (1+2+...+(num-1)) * (1/1+1/2+...+1/(num-1))
1+2+...+(num-1)=np.sum(np.arange(1,num))=num*(num-1)/2
1/1+1/2+...+1/(num-1)=np.true_divide (1,y)=np.reciprocal(y.astype(np.float64))

因此

def f5(num):
    return np.sum(np.reciprocal(np.arange(1, num).astype(np.float64))) * num*(num-1)/2
%timeit f5(9999)
106 µs ± 615 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

此外,大学数学可以将问题简化为更多计算机。

1/1+1/2+...+1/(num-1)=np.log(num-1)+1/(2*num-2)+np.euler_gamma
(n>2)

np.euler_gamma:Euler-Mascheroni常数(0.57721566 ...)

由于NumPy中的Euler-Mascheroni常数不准确,您将失去准确性,例如 489223499.9 991845 -> 489223500.0 408554 。 如果您可以忽略0.0000000085%的误差,则可以节省更多时间。

def f6(num):
    return (np.log(num-1)+1/(2*num-2)+np.euler_gamma)* num*(num-1)/2
%timeit f6(9999)
4.82 µs ± 29.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

输入越大,NumPy的好处就越大。

%timeit f3(99999)
56.7 s ± 590 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit f5(99999)
534 µs ± 86.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f5(99999999)
1.42 s ± 15.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
9.498947911958**416**e+16
%timeit f6(99999999)
4.88 µs ± 26.7 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
9.498947911958**506**e+16
%timeit f6(9999999999999999999)
17.9 µs ± 921 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

在特殊情况下,您可以使用numba(不幸的是,并非总是如此)。

from numba import jit
@jit
def f7(num):
    return (np.log(num-1)+1/(2*num-2)+np.euler_gamma)* num*(num-1)/2
# same code with f6(num)

%timeit f6(999999999999999)
5.63 µs ± 29.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
f7(123) # compile f7(num)
%timeit f7(999999999999999)
331 ns ± 1.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit f7(9999)
286 ns ± 3.09 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

所以,我建议同时使用NumPy,数学和numba。

答案 3 :(得分:5)

与Java(你只有这种反射机制)相比,Python的好处是有更多的灵活性(例如,类是对象)

这里没有提到Cython。它允许引入类型变量并将您的示例转换为C / C ++。然后它快得多。我也改变了循环中的界限......

from __future__ import division

cdef double total = 0.00
cdef int i, j
for i in range(9999):
    for j in range(1, 10000+i):
        total += (i / j)

from time import time
t = time()
print("total = %d" % total)
print("time = %f[s]" % (time() - t))

其次是

$ cython loops.pyx
$ gcc -I/usr/include/python2.7 -shared -pthread -fPIC -fwrapv -Wall -fno-strict-aliasing -O3 -o loops.so loops.c
$ python -c "import loops"

给出

total = 514219068
time = 0.000047[s]

答案 4 :(得分:4)

这是一种已知的现象 - python代码是动态的并且是解释的,java代码是静态类型和编译的。没有惊喜。

人们倾向于选择python的原因通常是:

  • 较小的代码库
  • 减少冗余(更多干)
  • 清洁代码

但是,如果你使用用C语言编写的库(来自python),性能可能会好得多(比较:picklecpickle)。

答案 5 :(得分:4)

您会发现列表推导或生成器表达式明显更快。例如:

total = sum(i / j for j in xrange(1, 9999) for i in xrange(9999))

这在我的机器上执行约11秒,而原始代码执行约26秒。仍然比Java慢一个数量级,但这更符合您的期望。

顺便说一下,通过将total初始化为0而不是0.0来使用整数而不是浮点加法,可以略微加快原始代码的速度。你的分区都有整数结果,所以将结果总结为浮点数没有意义。

在我的机器上,Psyco实际上将生成器表达式的速度降低到与原始循环相同的速度(它根本不会加速)。

答案 6 :(得分:4)

使用kindall的列表理解

total = sum(i / j for j in xrange(1, 9999) for i in xrange(9999))

是10.2秒并且使用pypy 1.7它 2.5 秒。这很有趣,因为pypy也将原版本加速到2.5秒。因此,对于pypy列表,理解将是过早的优化;)。好工作的pypy!

答案 7 :(得分:3)

Python for循环是静态类型和解释的。没编译。 Java更快,因为它具有Python没有的额外JIT加速功能。

http://en.wikipedia.org/wiki/Just-in-time_compilation

为了说明Java JIT的差异有多大,请看这个大约需要5分钟的python程序:

if __name__ =='__main__':
    total = 0.0
    i=1
    while i<=9999:
        j=1
        while j<=9999:
            total=1
            j+=1
        i+=1
    print total

虽然这个基本等效的Java程序大约需要23毫秒:

public class Main{
    public static void main(String args[]){
        float total = 0f; 

        long start_time = System.nanoTime();
        int i=1;

        while (i<=9999){
            int j=1;
            while(j<=9999){
                total+=1;
                j+=1;
            }
            i+=1;
        }
        long end_time = System.nanoTime();

        System.out.println("total: " + total);
        System.out.println("total milliseconds: " + 
           (end_time - start_time)/1000000);
    }
}

就在for循环中做任何事情而言,Java通过比1到1000个数量级更快的速度来清理python的时钟。

故事的道德:如果需要快速的性能,应该不惜一切代价避免基本的for循环python。这可能是因为Guido van Rossum希望鼓励人们使用多处理器友好的结构,如阵列拼接,其运行速度比Java快。

答案 8 :(得分:2)

如果您使用 While 循环 而不是 For 循环,执行速度会快得多(在 Python 3 中测试)。它将与执行相同操作的已编译 C 程序运行得一样快。 请尝试以下示例(MIPS 计算仅作参考,未考虑处理器架构等):


Python 3 程序


import time


N=100
i=0
j=0

StartTime=time.time()
while j<N:
    j=j+1
    while i<1000000:
        a=float(i)/float(j)
        i=i+1
EndTime=time.time()

DeltaTime=(EndTime-StartTime) # time in seconds


MIPS=(1/DeltaTime)*N



print("This program estimates the MIPS that your computational unit can perform")
print("------------------------------------------")
print("Execution Time in Seconds=",DeltaTime)
print("MIPS=",MIPS) 
print("------------------------------------------")



C 程序

#include <stdio.h>
#include <time.h>


int main(){

int i,j;
int N=100;
float a, DeltaTime, MIPS;
clock_t StartTime, EndTime;

StartTime=clock();

// This calculates n-time one million divisions

for (j=1;j<N; j++)
 {
    for(i=1;i<1000000;i=i+1)
     {
      a=(float)(i)/(float)(j);
     }
 }


EndTime=clock(); // measures time in microseconds

DeltaTime=(float)(EndTime - StartTime)/1000000;

MIPS=(1/DeltaTime)*N;

printf("------------------------------------------\n");
printf("Execution Time in Seconds=%f \n", DeltaTime);
printf("MIPS=%f \n", MIPS);
printf("------------------------------------------\n");

return 0;

}  

答案 9 :(得分:0)

使用python进行科学计算通常意味着在最关键的部分使用一些用C / C ++编写的计算软件,使用python作为内部脚本语言,如e.x. Sage(它也包含很多python代码)。

我认为这可能有用: http://blog.dhananjaynene.com/2008/07/performance-comparison-c-java-python-ruby-jython-jruby-groovy/

正如您所看到的,psyco / PyPy可以带来一定的改进,但仍然可能比C ++或Java慢得多。

答案 10 :(得分:0)

不确定是否已经进行了推荐,但我喜欢用列表推导替换for循环。它更快,更清洁,更pythonic。

http://www.pythonforbeginners.com/basics/list-comprehensions-in-python