【问题标题】:Why Python is so slow for a simple for loop?为什么 Python 对于一个简单的 for 循环来说这么慢?
【发布时间】:2011-12-27 04:46:29
【问题描述】:

我们正在 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

这个 sn-p 在我的电脑上需要 31.40 秒。

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

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

我应该使用诸如Psyco之类的JIT编译器吗?

编辑

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

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

【问题讨论】:

  • 您还可以使用 Boost::Python 等工具在 C 或 C++ 中实现部分代码
  • @FabioDiniz 谢谢,我考虑了。 +1
  • 任何解释型语言都将比编译型语言(如 Java)慢 1-2 个数量级。使它流行的是性能以外的因素。您并不总是需要高性能。如果你这样做了,但你也想要 python,你需要学习 C。
  • 尽管这是一个旧线程,但我发现我自己看了十几次.. 我不得不把它放到测试台上.. 现在使用 PyPy 1.9 这需要350 milliseconds我的电脑很普通。 Python 2.7 需要 8 seconds。当然,通过将代码放入函数中可以达到这种速度,因为这总是可以加快 python 中的速度。我还使用了from __future__ import division,因为它快得多

标签: python performance jit


【解决方案1】:

为什么在这个示例循环中 Java 比 Python 快?

新手说明:想象一个程序,例如货运列车,它在前进时铺设自己的火车轨道。在火车可以移动之前必须铺设轨道。 Java Freight列车可以在列车前方发送数千个轨道层,全部并行工作,提前数英里铺设轨道,而蟒蛇一次只能派遣一名劳工,并且只能在前方10英尺处铺设轨道。火车是。

Java 具有强大的类型,使编译器能够使用 JIT 功能:(https://en.wikipedia.org/wiki/Just-in-time_compilation),它使 CPU 能够在将来需要指令之前并行获取内存并执行指令。 Java 可以“某种程度上”在你的 for 循环中与自身并行运行指令。 Python 没有具体的类型,因此必须在每条指令中确定要完成的工作的性质。这会导致您的整个计算机停止并等待重新扫描所有变量中的所有内存。 python中的含义循环是多项式O(n^2)时间,而Java循环可以,而且通常是线性时间O(n),由于强类型。

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

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

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

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

【讨论】:

  • 为什么在这个示例循环中 Java 比 PyPy 快?你能提供一个理由/引文还是这是你的直觉?据我所知,PyPy 应该在关闭 SSE 优化的 GCC 范围内。
  • CPU cycles, memory accesses, C function calls, abstractions - 是否有资源列出[基本] CPython 操作的间接费用/足迹?它将为决定在关键代码中使用/在 Python 中做什么提供了很好的参考。
  • @naxa 我不知道有什么,我怀疑它的价值,至少如果它达到这么低的水平,因为它很快就会过时非常。每当有人更改解释器或运行时库的一部分时,这些数字就会发生变化(这种情况经常发生,即使在错误修复版本之间也是如此)。此外,所有这些额外的信息并不能真正帮助你,比如“局部变量查找比全局变量查找快几倍”(这更有可能保持有效)。如果两次额外的内存访问对您来说太多了,那么您不应该一开始就编写 Python。
  • 很难让这样的列表保持最新状态,这可能是个障碍;但除此之外,我认为在尝试优化代码的关键部分时,寻找效率低下的罪魁祸首会很有价值。那些不知道底层实现的人通常会尝试转向像 numpy 这样的 C 模块,但后来才意识到像在 numpy 数组上循环这样的事情是一个坏主意,原因有很多。也就是说,最好的选择可能是阅读解释器的源代码和/或根据经验将所有关键部分留在 C only 中(任务不与 python 代码混合)。
  • @naxa 找出现有效率低下的罪魁祸首通常最好通过分析来完成(如果cProfile 的开销太大,则有堆栈采样分析器)。是的,了解实现是在编写代码之前发现低效代码的好方法......如果这些知识被明智地应用。否则你会浪费时间优化不是瓶颈的东西。
【解决方案2】:

因为你提到了科学代码,所以看看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

【讨论】:

  • 谢谢。不错,但如果我制作大于 9999,例如 99999?它不是那么灵活吗?
  • 随心所欲地变大。但是请记住,这个特定示例将生成一个 x x x 数组,因此您最终会遇到内存限制。在这种情况下,您可以在一个轴上进行迭代(即一个循环而不是嵌套循环)并且仍然使用向量化操作。
  • 我问是因为 Python 说 ValueError: array is too big.
  • @naxa - 抱歉,我之前没有注意到您的评论! f2 对于大数来说较慢的原因是它构建了一个 num x num 2D 数组,它会占用大量内存。 f3 只建立一个 num-length 一维数组并重复复制它来进行除法。大数组的内存分配实际上是这里的瓶颈,所以f3 更快,因为它更容易在内存中为小数组找到连续空间。
  • @JimRaynor - 可能有一本类似的书,但我不知道。 Scipy-lectures 是对整个生态系统的一个很好的概述:scipy-lectures.github.io 然而,它并不是你所想的那样。另一个不错的选择是关注Jamie'sunutbu'sSven'sDSM's(等)numpy 答案。
【解决方案3】:

我认为 NumPy 可以比 CPython for 循环更快(我没有在 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:欧拉-马斯切罗尼常数(0.57721566...)

由于 NumPy 中 Euler-Mascheroni 常数的不准确性,您会失去准确性,例如 489223499.9991845 -> 489223500.0408554。 如果您可以忽略 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。

【讨论】:

    【解决方案4】:

    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 清理了 python 的时钟,速度提高了 1 到 1000 个数量级。

    故事的寓意:如果需要快速性能,应不惜一切代价避免基本的 Python for 循环。这可能是因为 Guido van Rossum 希望鼓励人们使用多处理器友好的构造,例如数组拼接,它的运行速度比 Java 快。

    【讨论】:

    • 那些while循环不是惯用的python。使用 for _ in 循环,这段代码使用 CPython 几乎快 3 倍,使用 pypy 只需几秒钟。
    • “快 1 到 1000 个数量级。” ..那么快1E1000倍?以前需要一秒钟的循环密集型操作,在 Java 中只需要 1E-1000 秒?这听起来很有吸引力。
    【解决方案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]
    

    【讨论】:

    • 你确定是0.000047s吗?在我的机器(i7 intel CPU)上,它是 0.42s。
    • 我在树莓派上测试了总 = 514219068 时间 = 0.000365[s]
    • 看起来您在打印 time() - t 之前定义了 t = time()。您需要在循环之前启动计时器。还是我对文件的编译方式有什么误解?
    【解决方案6】:

    这是一个众所周知的现象——python 代码是动态和解释的,java 代码是静态类型和编译的。没有惊喜。

    人们偏爱 python 的原因通常是:

    • 更小的代码库
    • 更少冗余(更多 DRY)
    • 更简洁的代码

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

    【讨论】:

    • 是的,我知道,但是为什么这么多人将它用于科学研究?只是图书馆支持?够了吗?以及如何减少执行时间,因为即使是一个小循环也很慢。我知道 Python 代码就像一个伪代码 [非常易读],但许多科学项目使用比这更大的循环。我可以写很多可读的、小而干净的代码,但它们的执行时间是无限的:)
    • @Thorn -- 有些人使用它是出于给定的原因,其他人可能有不同的原因。代码大小非常很重要,因为它与项目完成所需的时间直接相关。对许多人来说,性能不是唯一或最重要的因素。
    • 好的。我完全接受并尊重你的理由。但是请告诉我,上面的循环对于科学项目来说大吗?
    • @Thorn -- 这不是循环的大小,而是它是一个嵌套循环的事实,所以它确实如此 total += i / j (10 ** 4) * (10 ** 4) = 10 ** 8 次!很多吗?也许——有多少蛋白质结构沉积在 PDB 中? 10 ** 4?蛋白质有多少个原子——10 ** 3 或 10 ** 4?人类基因组有多大——10 ** 9?
    • @Thorn:它很大,但对于更大的科学计算项目来说并不算太古怪。这就是为什么从事此类项目的人会识别关键循环并将它们推送到 C 代码中。使用 Python 是因为 (1) 它适用于其他 80% 的代码,并且 (2) 拥有简洁的科学库,这些库要么已经解决了他们的特定问题,要么很容易与 Python 粘合在一起以完成他们需要的工作 ( NumPy)。结果可能比他们自己编写的要快(例如 NumPy 得到大量手写和优化的 C 和 FORTRAN 代码的支持)。
    【解决方案7】:

    您会发现列表推导式或生成器表达式要快得多。例如:

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

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

    顺便说一下,您的原始代码可以通过将total 初始化为0 而不是0.0 来使用整数而不是浮点加法来稍微加快速度。您的除法都有整数结果,因此没有必要将结果相加为浮点数。

    在我的机器上,Psyco 实际上减慢生成器表达式的速度,使其与原始循环的速度大致相同(它根本不会加速)。

    【讨论】:

      【解决方案8】:

      使用 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!

      【讨论】:

        【解决方案9】:

        不确定是否已提出建议,但我喜欢用列表理解替换 for 循环。它更快、更干净、更 Python 化。

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

        【讨论】:

          【解决方案10】:

          使用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 慢得多。

          【讨论】:

          • Sage 实际上做的是在 Cython 中编写大量代码。这是一种类似 Python 的语言(看起来很像并混合到相同的源代码中),但被反编译为 C/C++ Python 扩展。
          【解决方案11】:

          如果您使用 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;
          
          }  
          

          【讨论】:

          • 我运行了 3 个嵌套循环,范围为 300,while 和 for 循环在 python 3.8 中的执行情况相同。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-10-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多