列表理解,映射和numpy.vectorize性能

时间:2010-04-24 05:16:34

标签: python performance numpy list-comprehension

我有一个函数foo(i),它接受一个整数并花费大量时间来执行。以下任何一种初始化 a 的方式之间是否存在显着的性能差异:

a = [foo(i) for i in xrange(100)]

a = map(foo, range(100))

vfoo = numpy.vectorize(foo)
a = vfoo(range(100))

(我不关心输出是列表还是numpy数组。)

有更好的方法吗?

4 个答案:

答案 0 :(得分:18)

  • 为什么要对此进行优化?您是否编写过工作,经过测试的代码,然后检查了您的算法profiled您的代码并发现优化它会产生影响?你是在一个深层内循环中做到这一点,你发现你正在花时间吗?如果没有,请不要打扰。

  • 您只会通过计时来了解哪种方法最快。要以有用的方式计时,您必须专门针对您的实际用例。例如,您可以在列表推导中的函数调用与内联表达式之间获得明显的性能差异;目前尚不清楚你是否真的想要前者,或者你是否将其减少到相同的程度。

  • 你说无论你是以numpy数组还是list结束都没关系,但如果你正在进行这种微优化, >重要的是,因为随后使用它们会有不同的表现。把手指放在那里可能会很棘手,所以希望结果是整个问题都是不成熟的。

  • 为了清晰,可读性等,通常最好简单地使用正确的工具。我很难在这些事情之间做出决定。

    • 如果我需要numpy数组,我会使用它们。我会用它们来存储大型同构数组或多维数据。我经常使用它们,但很少我认为我想使用列表。
      • 如果我使用这些,我会尽力编写已经矢量化的函数,因此我不必使用numpy.vectorize。例如,下面的times_five可用于没有装饰的numpy数组。
    • 如果我没有理由使用numpy,也就是说如果我没有解决数值数学问题或使用特殊的numpy功能或存储多维数组或其他...
      • 如果我有已存在的功能,我会使用map。这就是它的用途。
      • 如果我的操作符合小表达式并且我不需要函数,我会使用列表解析。
      • 如果我只是想对所有情况进行操作,但实际上并不需要存储结果,我会使用plain for循环。
      • 在很多情况下,我实际上使用map和列表推导的懒惰等价物:itertools.imap和生成器表达式。在某些情况下,这些可以将内存使用量减少n,并且有时可以避免执行不必要的操作。

如果事实证明这就是性能问题所在,那么正确地做这件事是件棘手的事。人们为他们的实际问题计时错误的玩具箱是非常共同的。更糟糕的是,人们制定基于它的愚蠢的一般规则是非常普遍的。

考虑以下情况(timeme.py发布在下面)

python -m timeit "from timeme import x, times_five; from numpy import vectorize" "vectorize(times_five)(x)"
1000 loops, best of 3: 924 usec per loop

python -m timeit "from timeme import x, times_five" "[times_five(item) for item in x]"
1000 loops, best of 3: 510 usec per loop

python -m timeit "from timeme import x, times_five" "map(times_five, x)"
1000 loops, best of 3: 484 usec per loop

一个天真的观察者会得出结论,地图是这些选项中表现最好的,但答案仍然是“它取决于”。考虑使用您正在使用的工具的好处的能力:列表推导让您避免定义简单的功能;如果你正在做正确的事情,numpy允许你在C中对事物进行矢量化。

python -m timeit "from timeme import x, times_five" "[item + item + item + item + item for item in x]"
1000 loops, best of 3: 285 usec per loop

python -m timeit "import numpy; x = numpy.arange(1000)" "x + x + x + x + x"
10000 loops, best of 3: 39.5 usec per loop

但这不是全部 - 还有更多。考虑算法变化的力量。它可能更具戏剧性。

python -m timeit "from timeme import x, times_five" "[5 * item for item in x]"
10000 loops, best of 3: 147 usec per loop

python -m timeit "import numpy; x = numpy.arange(1000)" "5 * x"
100000 loops, best of 3: 16.6 usec per loop

有时,算法更改可能更有效。随着数字越来越大,这将越来越有效。

python -m timeit "from timeme import square, x" "map(square, x)"
10 loops, best of 3: 41.8 msec per loop

python -m timeit "from timeme import good_square, x" "map(good_square, x)"
1000 loops, best of 3: 370 usec per loop

即使是现在,这一切都可能对您的实际问题影响不大。如果你能正确使用numpy看起来很棒,但它有其局限性:这些numpy示例都没有使用数组中的实际Python对象。这使必须做的事情复杂化;很多甚至。如果我们确实使用C数据类型怎么办?这些不如Python对象强大。它们不可空。整数溢出。你必须做一些额外的工作来检索它们。他们是静态打字的。有时这些事情被证明是问题,甚至是意外问题。

所以,你去了:一个确定的答案。 “这取决于。”


# timeme.py

x = xrange(1000)

def times_five(a):
    return a + a + a + a + a

def square(a):
    if a == 0:
        return 0

    value = a
    for i in xrange(a - 1):
        value += a
    return value

def good_square(a):
    return a ** 2

答案 1 :(得分:9)

我的第一条评论是您应该在所有示例中使用xrange()或range()。如果你混合它们,那么你要比较苹果和橘子。

我的第二个@ Gabe的观点是,如果你有很多数据结构并且它们很大,那么numpy应该会赢得整体...只要记住大部分时间C比Python快,但是再次,大多数时间,PyPy比CPython快。 :-)

就listcomps与map()调用而言...一个进行101个函数调用而另一个进行102.你不会看到时间上的显着差异,如下所示使用 @Mark建议使用timeit 模块:

  • 列表理解

    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    1000000 loops, best of 3: 0.216 usec per loop
    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    1000000 loops, best of 3: 0.21 usec per loop
    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    1000000 loops, best of 3: 0.212 usec per loop

  • map()函数调用

    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    1000000 loops, best of 3: 0.216 usec per loop
    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    1000000 loops, best of 3: 0.214 usec per loop
    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    1000000 loops, best of 3: 0.215 usec per loop

尽管如此,我还会这样说:除非您计划使用从这些技术中创建的列表,否则我会完全避免它们。换句话说,如果你要做的就是迭代这些数据,那么当你只需要一次查看每个结果并尽快丢弃列表时,在内存中创建一个潜在的大量列表是不值得的额外内存消耗因为你已经完成了它。

在这种情况下,我强烈建议使用生成器表达式。 genexps不会在内存中创建整个列表...它是一种更加内存友好,懒惰的循环元素循环迭代方式。最好的部分是它的语法几乎与listcomps的语法相同:

a = (foo(i) for i in range(100))

沿着更多迭代的行,将剩余的2.x版本的所有range()调用更改为xrange(),然后在移植到Python 3时将其切换回range() xrange()替换并重命名为range():-)

答案 2 :(得分:7)

如果函数本身需要花费大量时间来执行,那么将其输出映射到数组的方式就无关紧要了。一旦你开始进入数百万个数字的数组,numpy可以为你节省大量的内存。

答案 3 :(得分:3)

列表理解是最快的,然后是地图,然后是我机器上的numpy。 numpy代码实际上比其他两个代码慢得多,但是如果你使用numpy.arange而不是range(或xrange),差异会小得多,就像我在下面列出的时间中所做的那样。另外,如果你使用psyco,列表理解会加快,而其他两个则会减慢速度。我还使用了比你的代码更大的数字数组,我的foo函数只计算了平方根。这是一些典型的时期。

没有psyco:

list comprehension: 47.5581952455 ms
map: 51.9082732582 ms
numpy.vectorize: 57.9601876775 ms

用psyco:

list comprehension: 30.4318844993 ms
map: 96.4504427239 ms
numpy.vectorize: 99.5858691538 ms

我使用了Python 2.6.4和timeit模块。

根据这些结果,我会说你选择哪一个初始化可能并没有什么不同。我可能会根据速度选择numpy或者列表理解,但最终你应该让你对数组的操作引导你的选择。