解释大型运行时变体

时间:2018-04-05 18:08:04

标签: python python-3.x numpy

我对以下分析结果感到有点困惑,并希望听到一些解释,以便理解它们。我想我会把内部产品作为一个简单的函数来比较不同的可能实现:

import numpy as np

def iterprod(a,b):
    for x,y in zip(a,b):
        yield x*y

def dot1(a,b):
    return sum([ x*y for x,y in zip(a,b) ])

def dot2(a,b):
    return sum(iterprod(a,b))

def dot3(a,b):
    return np.dot(a,b)

第一个实现dot1是一个天真的"一,我们首先创建一个新的成对产品列表,然后对其元素求和。我认为第二个实现dot2会更聪明,因为它消除了创建新列表的需要。第三个实现dot3使用Numpy的dot函数。

为了分析这些功能,我使用以下内容:

import timeit

def showtime( fun, a, b, rep=500 ):
    def wrapped():
        return fun(a,b)
    t = timeit.Timer( wrapped )
    print( t.timeit(rep) )

场景1:Python列表

import random
n = 100000

a = [ random.random() for k in range(n) ]
b = [ random.random() for k in range(n) ]

showtime( dot1, a, b )
showtime( dot2, a, b )
showtime( dot3, a, b )

输出:

3.883254656990175
3.9970695309893927
2.5059548830031417

所以"更聪明"实现dot2实际上比天真的dot1表现更差,而Numpy比两者都要快得多。但那时......

场景2:Python数组

我认为使用像array这样的数字容器可能会启用某些优化功能。

import array
a = array.array( 'd', a )
b = array.array( 'd', b )

showtime( dot1, a, b )
showtime( dot2, a, b )
showtime( dot3, a, b )

输出:

4.048957359002088
5.460344396007713
0.005460165994009003

不。如果有的话,它会使纯Python实现更糟糕,突出了"天真"之间的区别。和"智能"版本,现在Numpy 3个数量级更快!

问题

Q1。我能理解这些结果的唯一方法是,如果Numpy在场景1中处理数据之前实际上复制了数据,那么它只会"指向"在方案2中,这听起来合理吗?

Q2。为什么我的"聪明"实施系统性能比“天真”慢。一?如果我对Q1的预感是正确的,那么如果sum做了一些聪明的事情,那么创建新阵列的速度更快是完全可能的。是吗?

Q3。 3个数量级!怎么可能?我的实现真的很笨,还是有一些神奇的处理器指令来计算点积?

2 个答案:

答案 0 :(得分:3)

生成器/屈服机制确实耗费了一些CPU周期。当你不想一次想要整个序列时,它为你节省的是内存,或者当你想要交错几个相关的计算以降低你的延迟时间时也会有所帮助,也就是 first的时间< / em>序列中的项目。

在数组上使用numpy函数只是让它在连续的内存块上运行常规C代码,而不从列表中的指针解除引用float个对象。所以它变得非常快(这是numpy)的全部要点。

答案 1 :(得分:3)

Q1

Python列表包含指向python对象的指针,而数组则直接包含这些数字。然而,底层的numpy代码期望它是一个连续的数组。因此,当传递一个列表时,它必须将float中的值读取到列表中每个元素的新数组中。

如评论中所述,使用numpy的内置数组甚至更好。

Q2

从生成器获取值(从内存中)作为python函数调用的成本略低。这比列表理解要昂贵得多,除了x*y之外的所有内容都在解释器内处理。必须制作一份价格昂贵的清单,但这笔费用似乎很快就会减少。

Q3

Numpy的三个版本更快,因为它建立在非常高度优化的低级库上。根据使用的后端,它甚至可能使用多个线程。 Python必须处理大量的开销,以便在程序员的每一步都能让事情变得更容易,所以它真的不是一场公平竞赛。

加成

我提出了我的生成器建议,因为通常构建列表是一个重要的开销。但是,在这种情况下,它似乎没有实际意义,因为看起来列表上的sum()比迭代器上的更快。