列表推导式、map函数和numpy.vectorize函数的性能表现

19

我有一个函数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数组。)

有更好的方法吗?


15
为什么不试着计时呢? - mpen
4个回答

25
  • 为什么要对这个进行优化?你是否编写了可工作并经过测试的代码,然后分析了你的算法的性能并发现优化将产生影响?你是否在深层循环中执行此操作,且发现你正在花费时间?如果不是,请不要麻烦。

  • 只有通过计时,您才会知道哪个方法最快适合您。为了以有用的方式计时它,您必须将其专门用于实际用例。例如,在列表推导式中调用函数与内联表达式之间可能存在明显的性能差异;您不确定您是否真正需要前者或者您是否将其简化为使您的情况相似。

  • 您说无论您最终获得numpy数组还是list都没有关系,但如果您进行这种微小优化,那就会关系,因为在随后使用它们时,它们的性能会有所不同。找出问题可能有些棘手,因此希望整个问题都是过早的。

  • 通常最好只是使用正确的工具来提高清晰度、可读性等方面。很少有情况使我难以决定使用这些工具之间的哪一个。

    • 如果我需要numpy数组,我会使用它们。我会用它们来存储大型同种数组或多维数据。我经常使用它们,但很少在我认为我想使用列表的地方使用它们。
      • 如果我正在使用这些,我会尽力编写我的函数已经向量化,这样我就不必使用numpy.vectorize。例如,下面的times_five可以在numpy数组上使用,而不需要修饰。
    • 如果我没有理由使用numpy,也就是说,如果我不是解决数值数学问题、使用特殊的numpy功能或存储多维数组等...
      • 如果我有一个已存在的函数,我会使用map。那是它的作用。
      • 如果我有一个适合于小表达式内部的操作,而我不需要函数,我会使用列表推导式。
      • 如果我只想对所有情况执行该操作,但实际上并不需要存储结果,我会使用简单的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

一个天真的观察者可能会得出结论,map是这些选项中表现最佳的,但答案仍然是“取决于情况”。考虑使用你正在使用的工具的好处:列表生成式让你避免定义简单的函数;如果你正在做正确的事情,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对象健壮,它们不能为null值,整数会溢出,你需要做一些额外的工作才能检索到它们,它们是静态类型的。有时候这些东西会证明成问题,甚至是意想不到的问题。

所以,这就是一个明确的答案:"这取决于情况。"


# 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

13

首先评论:不要在您的示例中混合使用xrange()range()...这样做会使您的问题无效,因为您正在比较苹果和橙子。

我赞同@Gabe的观点,如果您有许多大型数据结构,则numpy应该总体上胜出...只需记住大多数情况下C比Python快,但是再次提醒,大多数情况下PyPy比CPython更快。:-)

至于列表推导式与map()调用的比较...一个需要进行101个函数调用,而另一个需要进行102个调用。这意味着您不会看到时间上的显着差异,如@Mike建议所示,使用timeit模块:

  • 列表推导式

    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    3次循环中,最佳用时:0.216 微秒/每次循环
    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    3次循环中,最佳用时:0.21 微秒/每次循环
    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    3次循环中,最佳用时:0.212 微秒/每次循环

  • map() 函数调用

    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    3次循环中,最佳用时:0.216 微秒/每次循环
    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    3次循环中,最佳用时:0.214 微秒/每次循环
    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    3次循环中,最佳用时:0.215 微秒/每次循环

话虽如此,除非您计划从这两种技术创建的列表中使用它们,否则请尽量避免使用它们(使用列表)。也就是说,如果您只是在它们上面进行迭代,那么当您仅关心逐个查看每个元素时,它不值得消耗内存(可能会创建一个潜在的大型列表)。完成后立即丢弃列表。

在这种情况下,我强烈建议使用生成器表达式,因为它们不会在内存中创建整个列表... 这是一种更加内存友好、惰性迭代的方式,用于循环处理元素而不创建一个较大的数组在内存中。最好的部分是它的语法几乎与listcomps相同:

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

仅适用于2.x用户:沿着更多迭代的方向,将所有 range()调用更改为 xrange()以适应任何旧的2.x代码,然后在移植到Python 3时切换到 range(),其中 xrange()被替换并重命名为 range()。


请注意,map需要定义一个函数,而列表推导则不需要,这可能是一个优点。一个实际的列表推导使用,它做与你的代码相同的事情,可能会内联f。在我的机器上,python -m timeit "def foo(x):pass; [None for i in range(100)]"给出的结果比你的列表推导使用快大约2/3的时间。这是否是OP想要的?我不知道,但这确实表明这些问题是微妙的,结论往往更多地反映了我们如何设计我们的示例,而不是任何真正用途的东西。 - Mike Graham
不完全正确。map()也不需要函数,例如map(None, range(100))。我有一个更长的关于性能和列表推导式与map()的演讲,但是OP没有问这个问题,所以我不能在这里回答。我可以说的是,为了真正加速列表推导式,你必须将那个函数简化为一个表达式并使用(而不是函数)。函数调用会有性能惩罚,在紧密循环中放大。 - wescpy
请记住,C语言总是比Python更快。请注意,这并不是真的。 - Mike Graham
我已经重新措辞了那个句子的语言。 - wescpy

7

如果函数本身执行需要很长时间,那么将其输出映射到数组中是无关紧要的。然而,一旦你开始处理数百万个数字的数组,numpy可以为你节省大量的内存。


同意...大量的数据结构在使用 C 语言处理时比纯 Python 处理更快。 - wescpy
2
请注意,使用 numpy.vectorize 实际上并不能像真正的 numpy 操作一样有效地将事物移动到 C 中。 - Mike Graham

4
列表推导式是最快的,然后是map函数,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或列表推导式的方法,但最终你应该让你对数组后续操作的需求来指导选择。

抱歉这么晚才回复。我一直在尝试发布,但是一直出现错误。 - Justin Peel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接